From 4225ff7d0ae7fb2e4d6c95412e51bb48693278b2 Mon Sep 17 00:00:00 2001 From: OMD-123 Date: Tue, 3 Nov 2026 22:04:20 +0530 Subject: [PATCH] fix(ext/web): reject non-object iterator results in URLSearchParams The `URLSearchParams` constructor built its pair list by calling `next()` on the input iterator and then reading `res.done`. It only checked for `null`/`undefined`, so a primitive iterator result was accepted. Web IDL's "create sequence from iterable" uses `IteratorStepValue`, which calls `IteratorNext`. `IteratorNext` throws a TypeError when the iterator result is not an Object, and it does so before `done` is read. A primitive result whose prototype happens to expose `done` was therefore treated as a completed iteration instead of raising a TypeError. Check that the iterator result is an object before reading `done`, so the constructor matches the Web IDL sequence conversion. Fixes #37117 --- ext/web/00_url.js | 2 +- tests/unit/url_search_params_test.ts | 51 +++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/ext/web/00_url.js b/ext/web/00_url.js index fa792a4c86f235..3540f72ffe5d88 100644 --- a/ext/web/00_url.js +++ b/ext/web/00_url.js @@ -159,7 +159,7 @@ class URLSearchParams { while (true) { // deno-lint-ignore deno-internal/prefer-primordials const res = iter.next(); - if (res == null) { + if (res == null || typeof res !== "object") { const err = new TypeError( "Each query pair must be an iterable [name, value] tuple", ); diff --git a/tests/unit/url_search_params_test.ts b/tests/unit/url_search_params_test.ts index b218428c40c60a..bcfcdbecb9d63c 100644 --- a/tests/unit/url_search_params_test.ts +++ b/tests/unit/url_search_params_test.ts @@ -1,5 +1,5 @@ // Copyright 2018-2026 the Deno authors. MIT license. -import { assert, assertEquals } from "./test_util.ts"; +import { assert, assertEquals, assertThrows } from "./test_util.ts"; // Regression test: set/delete on a list with many entries that share a name // must run in linear time. A quadratic implementation would take many seconds @@ -391,3 +391,52 @@ Deno.test(function urlSearchParamsInitNullIsStringified() { assertEquals(new URLSearchParams(null!).toString(), "null="); assertEquals(new URLSearchParams(null!).get("null"), ""); }); + +// Web IDL `create sequence from iterable` uses `IteratorStepValue`, which calls +// `IteratorNext`. `IteratorNext` throws a TypeError when the iterator result +// is not an Object, and it does so before `done` is read. +// +// The constructor used to check only for `null`/`undefined` and then read +// `res.done`, so a primitive iterator result whose prototype exposes `done` +// was accepted instead of throwing. +Deno.test(function urlSearchParamsRejectsNonObjectIteratorResult() { + let doneReads = 0; + const descriptor = Object.getOwnPropertyDescriptor( + Number.prototype, + "done", + ); + Object.defineProperty(Number.prototype, "done", { + configurable: true, + get() { + ++doneReads; + return true; + }, + }); + + try { + const iterable = { + [Symbol.iterator]() { + return { + next() { + return 1 as unknown as IteratorResult<[string, string]>; + }, + }; + }, + }; + + assertThrows( + () => new URLSearchParams(iterable), + TypeError, + "Each query pair must be an iterable [name, value] tuple", + ); + + // `done` must not be read on the primitive result. + assertEquals(doneReads, 0); + } finally { + if (descriptor) { + Object.defineProperty(Number.prototype, "done", descriptor); + } else { + delete (Number.prototype as unknown as { done?: unknown }).done; + } + } +});