From 99c8744458964fb146b062d0f64b31dba384fbc4 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Mon, 24 Aug 2026 15:36:28 +0800 Subject: [PATCH] webapi: URLSearchParams passes invalid percent-escapes through A '%' followed by two non-hex digits is passed through literally. So %2sf -> %2sf, not a hex decode error. --- src/browser/tests/net/url_search_params.html | 17 +++++++++++++++++ src/browser/webapi/net/URLSearchParams.zig | 16 ++++++++-------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/browser/tests/net/url_search_params.html b/src/browser/tests/net/url_search_params.html index 6eccd455a..4f782d0a7 100644 --- a/src/browser/tests/net/url_search_params.html +++ b/src/browser/tests/net/url_search_params.html @@ -635,3 +635,20 @@ testing.expectEqual('param1=1', usp.toString()); } + + diff --git a/src/browser/webapi/net/URLSearchParams.zig b/src/browser/webapi/net/URLSearchParams.zig index a41564864..0ad4274e4 100644 --- a/src/browser/webapi/net/URLSearchParams.zig +++ b/src/browser/webapi/net/URLSearchParams.zig @@ -315,16 +315,11 @@ fn unescape(arena: Allocator, value: []const u8, buf: []u8) !String { var in_i: usize = 0; while (in_i < value.len) { const b = value[in_i]; - if (b == '%') { - if (in_i + 2 >= value.len or !std.ascii.isHex(value[in_i + 1]) or !std.ascii.isHex(value[in_i + 2])) { - return error.InvalidEscapeSequence; - } + if (b == '%' and isEscapeTriplet(value, in_i)) { in_i += 3; unescaped_len -= 2; - } else if (b == '+') { - has_plus = true; - in_i += 1; } else { + has_plus = has_plus or b == '+'; in_i += 1; } } @@ -344,7 +339,7 @@ fn unescape(arena: Allocator, value: []const u8, buf: []u8) !String { in_i = 0; for (0..unescaped_len) |i| { const b = value[in_i]; - if (b == '%') { + if (b == '%' and isEscapeTriplet(value, in_i)) { out[i] = decodeHex(value[in_i + 1]) << 4 | decodeHex(value[in_i + 2]); in_i += 3; } else if (b == '+') { @@ -398,6 +393,11 @@ pub const Iterator = struct { } }; +// True when value[i] starts a valid %XX escape +fn isEscapeTriplet(value: []const u8, i: usize) bool { + return i + 2 < value.len and std.ascii.isHex(value[i + 1]) and std.ascii.isHex(value[i + 2]); +} + const GenericIterator = @import("../collections/iterator.zig").Entry; pub const KeyIterator = GenericIterator(Iterator, "0"); pub const ValueIterator = GenericIterator(Iterator, "1");