Skip to content

[wasm2js] Escape module names in generated JavaScript output#8377

Open
sumleo wants to merge 3 commits into
WebAssembly:mainfrom
sumleo:fix-wasm2js-string-escaping
Open

[wasm2js] Escape module names in generated JavaScript output#8377
sumleo wants to merge 3 commits into
WebAssembly:mainfrom
sumleo:fix-wasm2js-string-escaping

Conversation

@sumleo

@sumleo sumleo commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Summary

WebAssembly module and base names are arbitrary UTF-8 strings per the spec, and may contain JavaScript string metacharacters such as quotes, backslashes, and newlines. The wasm2js code generator was inserting these names directly into JavaScript string literals without escaping, which could produce syntactically invalid or exploitable JavaScript output.

This PR adds an escapeJSString() helper that escapes backslashes, single/double quotes, newlines, carriage returns, and the Unicode line/paragraph separators (U+2028, U+2029), and applies it at all three affected output points in src/wasm2js.h:

  1. emitPreES6 — ES6 import ... from '...' clause (single-quote string context). A module name containing ' would break out of the string literal.
  2. emitPostES6 — Object literal keys written as "..." for imported functions, memories, and tables. A module name containing " would break out.
  3. initActiveSegmentsimports['module']['base'] subscript expressions. Both module and base names are interpolated into single-quote strings without escaping.

Test plan

  • Full wasm2js test suite passes (python3 check.py --binaryen-bin ./build/bin wasm2js)
  • Clean build with no new warnings
  • Verified all three injection points are covered by reviewing every use of import->module and import->base in wasm2js.h

WebAssembly module and base names are arbitrary UTF-8 strings per the
spec, and may contain JavaScript string metacharacters such as quotes,
backslashes, and newlines. The wasm2js code generator was inserting
these names directly into JavaScript string literals without escaping,
which could produce syntactically invalid or exploitable JavaScript
output.

Add an escapeJSString() helper that escapes backslashes, single and
double quotes, newlines, carriage returns, and the Unicode line/paragraph
separators (U+2028, U+2029). Apply it at all three injection points:

1. emitPreES6: ES6 import 'from' clause (single-quote context)
2. emitPostES6: object literal keys (double-quote context) for
   imported functions, memories, and tables
3. initActiveSegments: imports['module']['base'] subscript access
   (single-quote context) for both module and base names
@kripken

kripken commented Feb 26, 2026

Copy link
Copy Markdown
Member

I'm not opposed to this if we have a user that would benefit. But AFAIK this has not come up because wasm2js is used on simple modules from LLVM, where this complexity is not needed.

Comment thread src/wasm2js.h
// Escape a string for safe inclusion in a JavaScript string literal.
// WebAssembly module/base names are arbitrary UTF-8 and may contain characters
// that are JS string metacharacters (quotes, backslashes, newlines, etc.).
static std::string escapeJSString(std::string_view str) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use printEscapedJSON from support/string.h instead? If not, that would be a better place to add new escaping logic.

@sumleo

sumleo commented Feb 26, 2026

Copy link
Copy Markdown
Contributor Author

Fair point - the primary motivation was hardening against unexpected input, not a specific user issue. I understand if this is not a priority given wasm2js usage patterns.

Regarding the inline comment: printEscapedJSON operates on WTF-16 input and only escapes double quotes, while we need to handle UTF-8 and also escape single quotes (for JS string contexts). I could move the escaping logic to support/string.h as a general printEscapedJS utility if this PR is worth keeping. Happy to close this if you think it is not needed.

@tlively

tlively commented Feb 26, 2026

Copy link
Copy Markdown
Member

I would be happy to accept the PR with the scaping moved to string.h/string.cpp. I can imagine that this will be useful again in the future.

@kripken

kripken commented Feb 26, 2026

Copy link
Copy Markdown
Member

Sounds ok to me with that refactoring, plus a test.

sumleo and others added 2 commits July 10, 2026 12:02
… string literals

Per review feedback, move the escaping logic into support/string.{h,cpp}
as a reusable String::printEscapedJS utility; wasm2js.h keeps a thin
escapeJSString wrapper so call sites are unchanged.

Also apply the escaping in cashew's printString, which previously
emitted string contents verbatim between double quotes. This covers the
imports object accesses in the asm.js function body (e.g.
imports["mod'ule"] in the --emscripten output path) that the earlier
commit missed.

Add gtest coverage for printEscapedJS and an end-to-end wasm2js golden
test (string_escapes.wast) with quote/backslash metacharacters in
import module and base names. Regenerating the wasm2js expectations
produced no changes to existing tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sumleo sumleo requested a review from a team as a code owner July 10, 2026 04:22
@sumleo sumleo requested review from tlively and removed request for a team July 10, 2026 04:22
@sumleo

sumleo commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Done in 5b6b5aa (plus a merge of latest main):

  • Escaping logic moved to support/string.{h,cpp} as String::printEscapedJS, following the printEscapedJSON ostream style. wasm2js.h keeps a thin escapeJSString wrapper so the call sites are unchanged.
  • While smoke-testing I found the --emscripten output path still emitted unescaped names: cashew's printString prints string contents verbatim, so imports["mod'ule\"x"] produced invalid JS. printString now uses the same utility. Regenerating the wasm2js expectations produced no changes to any existing test output.
  • Tests: gtest coverage for printEscapedJS (test/gtest/string.cpp — quotes, backslashes, newlines, U+2028/U+2029, truncated UTF-8 at end of string, non-ASCII passthrough) and an end-to-end golden test (test/wasm2js/string_escapes.wast) with quote/backslash metacharacters in import module/base names. The emitted .2asm.js passes node --check.

Comment thread src/support/string.cpp
Comment on lines +491 to +493
// Check for U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR)
// which are valid in JSON strings but act as line terminators in JS.
// They are encoded as E2 80 A8 and E2 80 A9 in UTF-8.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine there are many other characters we will want to escape so that the output remains human readable. Instead of adding a new escaping method, can we reuse printEscapedJSON, maybe by adding a flag for the encoding of the input? Alternatively, we could have printEscapedJS convert its input to WTF16 and then call printEscapedJSON. We can move this comment about LINE SEPARATOR and PARAGRAPH SEPARATOR into printEscapedJSON so we remember to continue escaping them in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants