From 3b224a24ed850e7d4ec587b7d1bcd0867caab7f4 Mon Sep 17 00:00:00 2001 From: DeepView Autofix <276251120+deepview-autofix@users.noreply.github.com> Date: Sun, 19 Apr 2026 03:55:31 +0300 Subject: [PATCH] fix: validate low surrogate in encodeString MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a high surrogate (0xD800–0xDBFF) was not followed by a valid low surrogate, the encoder still combined the next code unit into a supplementary code point, producing invalid UTF-8 percent-encoded output and swallowing the following character. Validate that the second code unit is a low surrogate (0xDC00–0xDFFF) and throw "URI malformed" otherwise, matching the behaviour for an unpaired trailing high surrogate. Co-Authored-By: Claude Co-Authored-By: DeepView Autofix <276251120+deepview-autofix@users.noreply.github.com> Co-Authored-By: Nikita Skovoroda Signed-off-by: Nikita Skovoroda --- lib/internals/querystring.js | 10 ++++++++-- test/stringify.test.ts | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/internals/querystring.js b/lib/internals/querystring.js index 35e6fd0..b01ba04 100644 --- a/lib/internals/querystring.js +++ b/lib/internals/querystring.js @@ -78,10 +78,16 @@ function encodeString(str) { throw new Error("URI malformed"); } - const c2 = str.charCodeAt(i) & 0x3ff; + const c2 = str.charCodeAt(i); + + // The second code unit must be a low surrogate (0xDC00–0xDFFF), + // otherwise the input is not a well-formed UTF-16 string. + if ((c2 & 0xfc00) !== 0xdc00) { + throw new Error("URI malformed"); + } lastPos = i + 1; - c = 0x10000 + (((c & 0x3ff) << 10) | c2); + c = 0x10000 + (((c & 0x3ff) << 10) | (c2 & 0x3ff)); out += hexTable[0xf0 | (c >> 18)] + hexTable[0x80 | ((c >> 12) & 0x3f)] + diff --git a/test/stringify.test.ts b/test/stringify.test.ts index dd3980a..9e24a18 100644 --- a/test/stringify.test.ts +++ b/test/stringify.test.ts @@ -76,6 +76,10 @@ test("invalid surrogate pair should throw", () => { assert.throws(() => qs.stringify({ foo: "\udc00" }), "URI malformed"); }); +test("high surrogate followed by non-surrogate should throw", () => { + assert.throws(() => qs.stringify({ foo: "a\ud800b" }), "URI malformed"); +}); + test("should omit nested values", () => { const f = qs.stringify({ a: "b",