Skip to content

Commit 1a0ae12

Browse files
adrian-niculescuAdrian Niculescu
authored andcommitted
fix: coerce names to USVString and brand-check iterator receivers
Review follow-up: - get()/getAll()/has()/delete() now coerce the name argument through the same USVString conversion as the optional value argument instead of casting it to a string blindly, so a number/boolean/object name works and a Symbol (or a throwing toString) raises a TypeError. - entries()/keys()/values() and the live iterator's next() brand-check their receivers and throw "Illegal invocation" for foreign objects, per the WebIDL iterable<> semantics. GetPointer also refuses to read an internal field from an object that has none (e.g. entries.call({})), which was an out-of-bounds read.
1 parent 00a2ef1 commit 1a0ae12

2 files changed

Lines changed: 119 additions & 8 deletions

File tree

test-app/app/src/main/assets/app/tests/testURLSearchParamsImpl.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,4 +411,65 @@ describe("Test URLSearchParams ", function () {
411411
expect(new URLSearchParams(undefined).toString()).toBe("");
412412
});
413413

414+
// --- The name argument is a USVString: coerced, not assumed. ---
415+
416+
it("Test URLSearchParams coerces a non-string name in get/getAll/has/delete", function(){
417+
const params = new URLSearchParams("1=a&true=b&null=c");
418+
expect(params.get(1)).toBe("a");
419+
expect(params.getAll(1).length).toBe(1);
420+
expect(params.getAll(1)[0]).toBe("a");
421+
expect(params.has(true)).toBe(true);
422+
expect(params.get(null)).toBe("c");
423+
params.delete(true);
424+
expect(params.has("true")).toBe(false);
425+
expect(params.has("1")).toBe(true);
426+
});
427+
428+
it("Test URLSearchParams coerces an object name via toString", function(){
429+
const params = new URLSearchParams("a=1");
430+
const name = { toString: function(){ return "a"; } };
431+
expect(params.get(name)).toBe("1");
432+
expect(params.has(name)).toBe(true);
433+
params.delete(name);
434+
expect(params.has("a")).toBe(false);
435+
});
436+
437+
it("Test URLSearchParams throws when the name is a Symbol", function(){
438+
const params = new URLSearchParams("a=1");
439+
expect(function(){ params.get(Symbol("x")); }).toThrow();
440+
expect(function(){ params.getAll(Symbol("x")); }).toThrow();
441+
expect(function(){ params.has(Symbol("x")); }).toThrow();
442+
expect(function(){ params.delete(Symbol("x")); }).toThrow();
443+
expect(params.get("a")).toBe("1");
444+
});
445+
446+
// --- Brand checks: iterators require a genuine receiver. ---
447+
448+
it("Test URLSearchParams entries/keys/values throw on a foreign receiver", function(){
449+
const params = new URLSearchParams("a=1");
450+
expect(function(){ params.entries.call({}); }).toThrow();
451+
expect(function(){ params.keys.call({}); }).toThrow();
452+
expect(function(){ params.values.call({}); }).toThrow();
453+
});
454+
455+
it("Test URLSearchParams iterator next() throws on a foreign receiver", function(){
456+
const params = new URLSearchParams("a=1");
457+
const iterator = params.entries();
458+
const next = iterator.next;
459+
expect(function(){ next.call({}); }).toThrow();
460+
// The iterator keeps working when invoked correctly afterwards.
461+
const first = iterator.next();
462+
expect(first.done).toBe(false);
463+
expect(first.value[0]).toBe("a");
464+
});
465+
466+
it("Test URLSearchParams entries retargets to another instance receiver", function(){
467+
const a = new URLSearchParams("a=1");
468+
const b = new URLSearchParams("b=2");
469+
const entries = [...a.entries.call(b)];
470+
expect(entries.length).toBe(1);
471+
expect(entries[0][0]).toBe("b");
472+
expect(entries[0][1]).toBe("2");
473+
});
474+
414475
});

test-app/runtime/src/main/cpp/URLSearchParamsImpl.cpp

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ namespace tns {
1010
URLSearchParamsImpl::URLSearchParamsImpl(ada::url_search_params params) : params_(params) {}
1111

1212
URLSearchParamsImpl *URLSearchParamsImpl::GetPointer(v8::Local<v8::Object> object) {
13+
// A foreign receiver (e.g. entries.call({})) has no internal field, and
14+
// reading one that is not there is undefined behavior, so check first
15+
// and report not-a-URLSearchParams instead.
16+
if (object->InternalFieldCount() < 1) {
17+
return nullptr;
18+
}
1319
auto ptr = object->GetAlignedPointerFromInternalField(0);
1420
if (ptr == nullptr) {
1521
return nullptr;
@@ -115,6 +121,7 @@ namespace tns {
115121
const char *const kStateSource = "src";
116122
const char *const kStateIndex = "idx";
117123
const char *const kStateKind = "knd";
124+
const char *const kStateIterator = "itr";
118125

119126
void IteratorSelf(const v8::FunctionCallbackInfo<v8::Value> &args) {
120127
// An iterator is itself iterable (iterator[@@iterator]() === iterator),
@@ -127,6 +134,17 @@ namespace tns {
127134
auto context = isolate->GetCurrentContext();
128135
auto state = args.Data().As<v8::Object>();
129136

137+
// WebIDL brand check: next() must be invoked on the iterator it was
138+
// created on, so a detached next() throws instead of advancing the
139+
// captured state.
140+
v8::Local<v8::Value> iteratorValue;
141+
if (!state->Get(context, ArgConverter::ConvertToV8String(isolate, kStateIterator))
142+
.ToLocal(&iteratorValue) ||
143+
!iteratorValue->StrictEquals(args.This())) {
144+
ThrowTypeError(isolate, "Illegal invocation");
145+
return;
146+
}
147+
130148
auto indexKey = ArgConverter::ConvertToV8String(isolate, kStateIndex);
131149
v8::Local<v8::Value> sourceValue;
132150
v8::Local<v8::Value> indexValue;
@@ -180,6 +198,13 @@ namespace tns {
180198
auto isolate = args.GetIsolate();
181199
auto context = isolate->GetCurrentContext();
182200

201+
// WebIDL brand check: entries()/keys()/values() require a genuine
202+
// URLSearchParams receiver, so e.g. entries.call({}) throws.
203+
if (URLSearchParamsImpl::GetPointer(args.This()) == nullptr) {
204+
ThrowTypeError(isolate, "Illegal invocation");
205+
return;
206+
}
207+
183208
auto state = v8::Object::New(isolate);
184209
state->Set(context, ArgConverter::ConvertToV8String(isolate, kStateSource),
185210
args.This()).Check();
@@ -196,6 +221,9 @@ namespace tns {
196221
}
197222

198223
auto iterator = v8::Object::New(isolate);
224+
// Recorded in the hidden state so next() can brand-check its receiver.
225+
state->Set(context, ArgConverter::ConvertToV8String(isolate, kStateIterator),
226+
iterator).Check();
199227
iterator->Set(context, ArgConverter::ConvertToV8String(isolate, "next"), nextFn).Check();
200228
iterator->Set(context, v8::Symbol::GetIterator(isolate), selfFn).Check();
201229
iterator->DefineOwnProperty(context, v8::Symbol::GetToStringTag(isolate),
@@ -525,7 +553,13 @@ namespace tns {
525553
if (ptr == nullptr) {
526554
return;
527555
}
528-
auto key = ArgConverter::ConvertToString(args[0].As<v8::String>());
556+
// The name is a USVString (url.bs:3861): coerce it like the optional
557+
// value below instead of assuming a string (a Symbol or throwing
558+
// toString leaves the pending exception and aborts).
559+
std::string key;
560+
if (!ValueToString(args.GetIsolate()->GetCurrentContext(), args[0], key)) {
561+
return;
562+
}
529563
// Spec delete(name, value): when the value argument is given, remove only
530564
// tuples matching BOTH name and value (url.bs:4000). The value is a
531565
// USVString, so coerce via ValueToString (number/boolean -> string; a
@@ -590,8 +624,13 @@ namespace tns {
590624
args.GetReturnValue().SetUndefined();
591625
return;
592626
}
593-
auto key = args[0].As<v8::String>();
594-
auto value = ptr->GetURLSearchParams()->get(ArgConverter::ConvertToString(key));
627+
// The name is a USVString (url.bs:3862); a Symbol or throwing toString
628+
// leaves the pending exception and aborts.
629+
std::string key;
630+
if (!ValueToString(isolate->GetCurrentContext(), args[0], key)) {
631+
return;
632+
}
633+
auto value = ptr->GetURLSearchParams()->get(key);
595634
if (value.has_value()) {
596635
auto ret = ArgConverter::ConvertToV8String(isolate, std::string(value.value()));
597636
args.GetReturnValue().Set(ret);
@@ -610,8 +649,13 @@ namespace tns {
610649
args.GetReturnValue().Set(v8::Array::New(isolate));
611650
return;
612651
}
613-
auto key = args[0].As<v8::String>();
614-
auto values = ptr->GetURLSearchParams()->get_all(ArgConverter::ConvertToString(key));
652+
// The name is a USVString (url.bs:3863); a Symbol or throwing toString
653+
// leaves the pending exception and aborts.
654+
std::string key;
655+
if (!ValueToString(context, args[0], key)) {
656+
return;
657+
}
658+
auto values = ptr->GetURLSearchParams()->get_all(key);
615659
auto ret = v8::Array::New(isolate, values.size());
616660
size_t i = 0;
617661
for (auto item: values) {
@@ -626,7 +670,13 @@ namespace tns {
626670
args.GetReturnValue().Set(false);
627671
return;
628672
}
629-
auto key = args[0].As<v8::String>();
673+
// The name is a USVString (url.bs:3864): coerce it like the optional
674+
// value below instead of assuming a string (a Symbol or throwing
675+
// toString leaves the pending exception and aborts).
676+
std::string key;
677+
if (!ValueToString(args.GetIsolate()->GetCurrentContext(), args[0], key)) {
678+
return;
679+
}
630680
// Spec has(name, value): when the value argument is given, return true
631681
// only for a tuple matching BOTH name and value (url.bs:4028). The value
632682
// is a USVString, so coerce via ValueToString (number/boolean -> string;
@@ -640,9 +690,9 @@ namespace tns {
640690
if (!ValueToString(args.GetIsolate()->GetCurrentContext(), args[1], filter)) {
641691
return; // coercion threw (Symbol / throwing toString)
642692
}
643-
value = ptr->GetURLSearchParams()->has(ArgConverter::ConvertToString(key), filter);
693+
value = ptr->GetURLSearchParams()->has(key, filter);
644694
} else {
645-
value = ptr->GetURLSearchParams()->has(ArgConverter::ConvertToString(key));
695+
value = ptr->GetURLSearchParams()->has(key);
646696
}
647697

648698
args.GetReturnValue().Set(value);

0 commit comments

Comments
 (0)