From 41f4cfbe520881846be6dd319e5985d1670896d9 Mon Sep 17 00:00:00 2001 From: Yi LIU Date: Wed, 25 Feb 2026 21:54:16 +0800 Subject: [PATCH] [wasm2js] Escape module names in generated JavaScript output 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 --- src/wasm2js.h | 62 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/src/wasm2js.h b/src/wasm2js.h index 2ae0b85e021..f37a669bc9c 100644 --- a/src/wasm2js.h +++ b/src/wasm2js.h @@ -2628,6 +2628,55 @@ void Wasm2JSBuilder::addMemoryGrowFunc(Ref ast, Module* wasm) { ast->push_back(memoryGrowFunc); } +// 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) { + std::string result; + result.reserve(str.size()); + for (size_t i = 0; i < str.size(); i++) { + unsigned char c = str[i]; + switch (c) { + case '\\': + result += "\\\\"; + break; + case '\'': + result += "\\'"; + break; + case '"': + result += "\\\""; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + default: + // 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. + if (c == 0xE2 && i + 2 < str.size() && + static_cast(str[i + 1]) == 0x80) { + unsigned char c2 = str[i + 2]; + if (c2 == 0xA8) { + result += "\\u2028"; + i += 2; + break; + } + if (c2 == 0xA9) { + result += "\\u2029"; + i += 2; + break; + } + } + result += static_cast(c); + break; + } + } + return result; +} + // Wasm2JSBuilder emits the core of the module - the functions etc. that would // be the asm.js function in an asm.js world. This class emits the rest of the // "glue" around that. @@ -2708,7 +2757,7 @@ void Wasm2JSGlue::emitPreES6() { baseModuleMap[base] = module; if (seenModules.count(module) == 0) { out << "import * as " << asmangle(module.toString()) << " from '" - << module << "';\n"; + << escapeJSString(module.toString()) << "';\n"; seenModules.insert(module); } }; @@ -2770,7 +2819,7 @@ void Wasm2JSGlue::emitPostES6() { if (seenModules.count(import->module) > 0) { return; } - out << " \"" << import->module + out << " \"" << escapeJSString(import->module.toString()) << "\": " << asmangle(import->module.toString()) << ",\n"; seenModules.insert(import->module); }); @@ -2781,7 +2830,7 @@ void Wasm2JSGlue::emitPostES6() { if (ABI::wasm2js::isHelper(import->base)) { return; } - out << " \"" << import->module << "\": {\n"; + out << " \"" << escapeJSString(import->module.toString()) << "\": {\n"; out << " " << asmangle(import->base.toString()) << ": { buffer : mem" << moduleName.str << " }\n"; out << " },\n"; @@ -2796,7 +2845,7 @@ void Wasm2JSGlue::emitPostES6() { if (seenModules.count(import->module) > 0) { return; } - out << " \"" << import->module + out << " \"" << escapeJSString(import->module.toString()) << "\": " << asmangle(import->module.toString()) << ",\n"; seenModules.insert(import->module); }); @@ -2900,8 +2949,9 @@ void Wasm2JSGlue::emitMemory() { if (auto* get = segment.offset->dynCast()) { auto internalName = get->name; auto importedGlobal = wasm.getGlobal(internalName); - return std::string("imports['") + importedGlobal->module.toString() + - "']['" + importedGlobal->base.toString() + "']"; + return std::string("imports['") + + escapeJSString(importedGlobal->module.toString()) + "']['" + + escapeJSString(importedGlobal->base.toString()) + "']"; } Fatal() << "non-constant offsets aren't supported yet\n"; };