From 7edb09b62949c1dd85c27e84ab42ec218beae80c Mon Sep 17 00:00:00 2001 From: Samuele Casarin Date: Fri, 27 Mar 2026 19:20:35 +0100 Subject: [PATCH 1/5] Add mochitests for #361 --- taint/test/mochitest/mochitest.ini | 2 ++ taint/test/mochitest/test_normalize.html | 28 ++++++++++++++++++++++ taint/test/mochitest/test_toLowerCase.html | 28 ++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 taint/test/mochitest/test_normalize.html create mode 100644 taint/test/mochitest/test_toLowerCase.html diff --git a/taint/test/mochitest/mochitest.ini b/taint/test/mochitest/mochitest.ini index a6989309fd508..90b0ac76a8306 100644 --- a/taint/test/mochitest/mochitest.ini +++ b/taint/test/mochitest/mochitest.ini @@ -61,3 +61,5 @@ scheme = https [test_network_request_url_argument.html] [test_string_iterator.html] [test_json_stringify.html] +[test_toLowerCase.html] +[test_normalize.html] diff --git a/taint/test/mochitest/test_normalize.html b/taint/test/mochitest/test_normalize.html new file mode 100644 index 0000000000000..0f8ea6d69401a --- /dev/null +++ b/taint/test/mochitest/test_normalize.html @@ -0,0 +1,28 @@ + + + + + + Test normalize (see + https://github.com/SAP/project-foxhound/issues/361) + + + + + + + + + diff --git a/taint/test/mochitest/test_toLowerCase.html b/taint/test/mochitest/test_toLowerCase.html new file mode 100644 index 0000000000000..cf8fd475bd429 --- /dev/null +++ b/taint/test/mochitest/test_toLowerCase.html @@ -0,0 +1,28 @@ + + + + + + Test toLowerCase (see + https://github.com/SAP/project-foxhound/issues/361) + + + + + + + + + From a2647d38323a6affe8bedfc75753e5360cdfb2fa Mon Sep 17 00:00:00 2001 From: Samuele Casarin Date: Fri, 27 Mar 2026 19:21:00 +0100 Subject: [PATCH 2/5] Fix flawed taint propagation with toLowerCase and normalize (fixes #361) --- js/src/builtin/String.cpp | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/js/src/builtin/String.cpp b/js/src/builtin/String.cpp index 9e36292907e99..729a96af0722d 100644 --- a/js/src/builtin/String.cpp +++ b/js/src/builtin/String.cpp @@ -1172,16 +1172,11 @@ static size_t ToLowerCaseLength(const char16_t* chars, size_t startIndex, } template -static JSLinearString* ToLowerCase(JSContext* cx, JSLinearString* str) { +static JSLinearString* ToLowerCaseInternal(JSContext* cx, JSLinearString* str) { // Unlike toUpperCase, toLowerCase has the nice invariant that if the // input is a Latin-1 string, the output is also a Latin-1 string. StringChars newChars(cx); - // Foxhound: cache the taint up here to prevent GC issues - SafeStringTaint taint(str->taint()); - if (taint.hasTaint()) { - taint.extend(TaintOperationFromContextJSString(cx, "toLowerCase", str)); - } const size_t length = str->length(); size_t resultLength; @@ -1234,11 +1229,7 @@ static JSLinearString* ToLowerCase(JSContext* cx, JSLinearString* str) { } // If no character needs to change, return the input string. - // Foxhound: disabled. We need to return a new string here (so we can correctly - // set the taint). However, we are in an AutoCheckCannotGC block, so cannot - // allocate a new string here. if (i == length) { - str->setTaint(cx, taint); return str; } @@ -1269,15 +1260,23 @@ static JSLinearString* ToLowerCase(JSContext* cx, JSLinearString* str) { } } - // Foxhound: Add taint operation to all taint ranges of the input string. JSLinearString* res = newChars.template toStringDontDeflate(cx, resultLength); - if (res && taint.hasTaint()) { - res->setTaint(cx, taint); - } return res; } +template +static JSLinearString* ToLowerCase(JSContext* cx, JSLinearString* str) { + JSLinearString* res = ToLowerCaseInternal(cx, str); + if (res == str) { + res = NewDependentString(cx, str, 0, str->length()); + } + SafeStringTaint taint(str->taint()); + taint.extend(TaintOperationFromContextJSString(cx, "toLowerCase", true, str)); + res->setTaint(taint); + return res; +} + JSLinearString* js::StringToLowerCase(JSContext* cx, JSString* string) { JSLinearString* linear = string->ensureLinear(cx); if (!linear) { @@ -1931,14 +1930,16 @@ static bool str_localeCompare(JSContext* cx, unsigned argc, Value* vp) { static bool str_normalize(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "normalize"); CallArgs args = CallArgsFromVp(argc, vp); - + // Steps 1-2. - RootedString str(cx, + RootedString arg(cx, ToStringForStringFunction(cx, "normalize", args.thisv())); - if (!str) { + if (!arg) { return false; } + auto* str = NewDependentString(cx, arg, 0, arg->length()); + using NormalizationForm = mozilla::intl::String::NormalizationForm; NormalizationForm form; From a64e8409808d7b33d124aee73b15085dac8f3995 Mon Sep 17 00:00:00 2001 From: Samuele Casarin Date: Mon, 30 Mar 2026 12:43:43 +0200 Subject: [PATCH 3/5] Extend taint if input is tainted in ToLowerCase --- js/src/builtin/String.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/js/src/builtin/String.cpp b/js/src/builtin/String.cpp index 729a96af0722d..02f38cf10f0ab 100644 --- a/js/src/builtin/String.cpp +++ b/js/src/builtin/String.cpp @@ -1268,12 +1268,14 @@ static JSLinearString* ToLowerCaseInternal(JSContext* cx, JSLinearString* str) { template static JSLinearString* ToLowerCase(JSContext* cx, JSLinearString* str) { JSLinearString* res = ToLowerCaseInternal(cx, str); - if (res == str) { - res = NewDependentString(cx, str, 0, str->length()); + if (res && str->isTainted()) { + if (res == str) { + res = NewDependentString(cx, str, 0, str->length()); + } + SafeStringTaint taint(str->taint()); + taint.extend(TaintOperationFromContextJSString(cx, "toLowerCase", true, str)); + res->setTaint(taint); } - SafeStringTaint taint(str->taint()); - taint.extend(TaintOperationFromContextJSString(cx, "toLowerCase", true, str)); - res->setTaint(taint); return res; } From 7386b57de9eb8c5944ec5a70ee98edbabe7e09af Mon Sep 17 00:00:00 2001 From: Samuele Casarin Date: Mon, 30 Mar 2026 14:27:10 +0200 Subject: [PATCH 4/5] Use NewDependentString if input is tainted in str_normalize. Minor fixes --- js/src/builtin/String.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/js/src/builtin/String.cpp b/js/src/builtin/String.cpp index 02f38cf10f0ab..6d812d49efa03 100644 --- a/js/src/builtin/String.cpp +++ b/js/src/builtin/String.cpp @@ -1271,6 +1271,9 @@ static JSLinearString* ToLowerCase(JSContext* cx, JSLinearString* str) { if (res && str->isTainted()) { if (res == str) { res = NewDependentString(cx, str, 0, str->length()); + if (!res) { + return nullptr; + } } SafeStringTaint taint(str->taint()); taint.extend(TaintOperationFromContextJSString(cx, "toLowerCase", true, str)); @@ -1934,13 +1937,19 @@ static bool str_normalize(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Steps 1-2. - RootedString arg(cx, + RootedString str(cx, ToStringForStringFunction(cx, "normalize", args.thisv())); - if (!arg) { + if (!str) { return false; } - auto* str = NewDependentString(cx, arg, 0, arg->length()); + if (str->isTainted()) { + JSString* dep = NewDependentString(cx, str, 0, str->length()); + if (!dep) { + return false; + } + str = dep; + } using NormalizationForm = mozilla::intl::String::NormalizationForm; From 1b60345aa2d9724c561794c69f534257fce7fd3c Mon Sep 17 00:00:00 2001 From: Samuele Casarin Date: Mon, 30 Mar 2026 15:33:25 +0200 Subject: [PATCH 5/5] Minor fix after rebase --- js/src/builtin/String.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/builtin/String.cpp b/js/src/builtin/String.cpp index 6d812d49efa03..76281990745d9 100644 --- a/js/src/builtin/String.cpp +++ b/js/src/builtin/String.cpp @@ -1276,7 +1276,7 @@ static JSLinearString* ToLowerCase(JSContext* cx, JSLinearString* str) { } } SafeStringTaint taint(str->taint()); - taint.extend(TaintOperationFromContextJSString(cx, "toLowerCase", true, str)); + taint.extend(TaintOperationFromContextJSString(cx, "toLowerCase", str)); res->setTaint(taint); } return res; @@ -1935,7 +1935,7 @@ static bool str_localeCompare(JSContext* cx, unsigned argc, Value* vp) { static bool str_normalize(JSContext* cx, unsigned argc, Value* vp) { AutoJSMethodProfilerEntry pseudoFrame(cx, "String.prototype", "normalize"); CallArgs args = CallArgsFromVp(argc, vp); - + // Steps 1-2. RootedString str(cx, ToStringForStringFunction(cx, "normalize", args.thisv()));