tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 0ccab1afc3775c6fa748725589d2c88fec5ba008
parent 55970eed95b4e319f0b7bad4e5a480fc17017f01
Author: Julian Descottes <jdescottes@mozilla.com>
Date:   Sat, 29 Nov 2025 10:37:40 +0000

Bug 1941780 - Add JSShell test for Frame.eval bypassCSP option r=arai

Differential Revision: https://phabricator.services.mozilla.com/D274097

Diffstat:
Ajs/src/jit-test/tests/debug/Frame-eval-csp-bypass.js | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mjs/src/shell/js.cpp | 37++++++++++++++++++++++++++++++++++++-
2 files changed, 190 insertions(+), 1 deletion(-)

diff --git a/js/src/jit-test/tests/debug/Frame-eval-csp-bypass.js b/js/src/jit-test/tests/debug/Frame-eval-csp-bypass.js @@ -0,0 +1,154 @@ +// Test that debugger evaluation can bypass CSP restrictions when bypassCSP option is set. + +var g = newGlobal({newCompartment: true}); +var dbg = new Debugger(g); + +// Create a function to trigger a debugger statement before enabling CSP. +g.eval("function triggerDebugger() { debugger; }"); + +// Create functions that will use eval/new Function and which will be called +// from Frame.eval. +g.eval("function triggerEval() { return eval('2 + 2'); }"); +g.eval("function triggerNewFunction() { return new Function('return 2 + 2')(); }"); + +const EXPECTED_VALUE = 4; +function assertSuccess(expression, options = {}) { + let evaluated = false; + dbg.onDebuggerStatement = function (frame) { + assertEq(frame.eval(expression, options).return, EXPECTED_VALUE); + evaluated = true; + }; + g.triggerDebugger(); + assertEq(evaluated, true); +} + +function assertThrows(expression, options = {}) { + let evaluated = false; + dbg.onDebuggerStatement = function (frame) { + assertEq(frame.eval(expression, options).throw !== undefined, true); + evaluated = true; + }; + g.triggerDebugger(); + assertEq(evaluated, true); +} + +// Smoke test without enabling CSP + +// evaluation without eval/new Function always succeeds +assertSuccess("2 + 2"); +assertSuccess("2 + 2", { bypassCSP: false }); +assertSuccess("2 + 2", { bypassCSP: true }); + +// Default value for bypassCSP +assertSuccess("eval('2 + 2')"); +assertSuccess("new Function('return 2 + 2')()"); +assertSuccess("triggerEval()"); +assertSuccess("triggerNewFunction()"); + +// bypassCSP=false +assertSuccess("eval('2 + 2')", { bypassCSP: false }); +assertSuccess("new Function('return 2 + 2')()", { bypassCSP: false }); +assertSuccess("triggerEval()", { bypassCSP: false }); +assertSuccess("triggerNewFunction()", { bypassCSP: false }); + +// bypassCSP=true +assertSuccess("eval('2 + 2')", { bypassCSP: true }); +assertSuccess("new Function('return 2 + 2')()", { bypassCSP: true }); +assertSuccess("triggerEval()", { bypassCSP: true }); +assertSuccess("triggerNewFunction()", { bypassCSP: true }); + +// Enable CSP +setCSPEnabled(true); + +// evaluation without eval/new Function always succeeds +assertSuccess("2 + 2"); +assertSuccess("2 + 2", { bypassCSP: false }); +assertSuccess("2 + 2", { bypassCSP: true }); + +// Default value for bypassCSP, should all fail +assertThrows("eval('2 + 2')"); +assertThrows("new Function('return 2 + 2')()"); +assertThrows("triggerEval()"); +assertThrows("triggerNewFunction()"); + +// bypassCSP=false, should all fail +assertThrows("eval('2 + 2')", { bypassCSP: false }); +assertThrows("new Function('return 2 + 2')()", { bypassCSP: false }); +assertThrows("triggerEval()", { bypassCSP: false }); +assertThrows("triggerNewFunction()", { bypassCSP: false }); + +// bypassCSP=true, should all succeed +assertSuccess("eval('2 + 2')", { bypassCSP: true }); +assertSuccess("new Function('return 2 + 2')()", { bypassCSP: true }); +assertSuccess("triggerEval()", { bypassCSP: true }); +assertSuccess("triggerNewFunction()", { bypassCSP: true }); + +// Define a few functions using eval with and without bypassCSP. +// They should both behave the same when triggered from a later Frame.eval, and +// only the bypassCSP flag for the later Frame.eval should matter. +dbg.onDebuggerStatement = function (frame) { + frame.eval("globalThis.fnWithBypass = () => eval('2 + 2')", { bypassCSP: true }); + frame.eval("globalThis.fnWithoutBypass = () => eval('2 + 2')", { bypassCSP: false }); +}; +g.triggerDebugger(); + +// Call the function defined with bypassCSP=true, should only work when +// bypassCSP is true for the current Frame.eval. +assertThrows("globalThis.fnWithBypass()"); +assertThrows("globalThis.fnWithBypass()", { bypassCSP: false }); +assertSuccess("globalThis.fnWithBypass()", { bypassCSP: true }); + +// Call the function defined with bypassCSP=false, should only work when +// bypassCSP is true for the current Frame.eval. +assertThrows("globalThis.fnWithoutBypass()"); +assertThrows("globalThis.fnWithoutBypass()", { bypassCSP: false }); +assertSuccess("globalThis.fnWithoutBypass()", { bypassCSP: true }); + +// Test for async frames. +const asyncEvalExpression = `(async function() { +try { + globalThis.evalBeforeAwaitSuccess = false; + globalThis.evalAfterAwaitSuccess = false; + eval("1 + 1"); + globalThis.evalBeforeAwaitSuccess = true; + await 1; + eval("2 + 2"); + globalThis.evalAfterAwaitSuccess = true; +} catch {} +})()`; + +// Test async code with bypassCSP=true, only the eval before the await should be +// successful. +dbg.onDebuggerStatement = function (frame) { + frame.onPop = completion => { + frame.eval(asyncEvalExpression, { bypassCSP: true }); + dbg.removeDebuggee(g); // avoid the DebuggeeWouldRun exception + drainJobQueue(); + dbg.addDebuggee(g); + } +}; +g.triggerDebugger(); + +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.eval("globalThis.evalBeforeAwaitSuccess", options).return, true); + assertEq(frame.eval("globalThis.evalAfterAwaitSuccess", options).return, false); +}; +g.triggerDebugger(); + +// Test async code with bypassCSP=false, all eval should fail. +dbg.onDebuggerStatement = function (frame) { + frame.onPop = completion => { + frame.eval(asyncEvalExpression, { bypassCSP: false }); + dbg.removeDebuggee(g); // avoid the DebuggeeWouldRun exception + drainJobQueue(); + dbg.addDebuggee(g); + } +}; +g.triggerDebugger(); +drainJobQueue(); + +dbg.onDebuggerStatement = function (frame) { + assertEq(frame.eval("globalThis.evalBeforeAwaitSuccess", options).return, false); + assertEq(frame.eval("globalThis.evalAfterAwaitSuccess", options).return, false); +}; +g.triggerDebugger(); diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp @@ -960,8 +960,23 @@ class ShellPrincipals final : public JSPrincipals { static ShellPrincipals fullyTrusted; }; +// Global CSP state for the shell. When true, CSP restrictions are enforced. +static bool gCSPEnabled = false; + +static bool ContentSecurityPolicyAllows( + JSContext* cx, JS::RuntimeCode kind, JS::Handle<JSString*> codeString, + JS::CompilationType compilationType, + JS::Handle<JS::StackGCVector<JSString*>> parameterStrings, + JS::Handle<JSString*> bodyString, + JS::Handle<JS::StackGCVector<JS::Value>> parameterArgs, + JS::Handle<JS::Value> bodyArg, bool* outCanCompileStrings) { + // If CSP is enabled, block string compilation. + *outCanCompileStrings = !gCSPEnabled; + return true; +} + JSSecurityCallbacks ShellPrincipals::securityCallbacks = { - nullptr, // contentSecurityPolicyAllows + ContentSecurityPolicyAllows, nullptr, // codeForEvalGets subsumes}; @@ -1654,6 +1669,20 @@ static bool SetTimeout(JSContext* cx, unsigned argc, Value* vp) { return true; } +static bool SetCSPEnabled(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + if (args.length() != 1) { + JS_ReportErrorASCII(cx, "setCSPEnabled() requires one boolean argument"); + return false; + } + + gCSPEnabled = JS::ToBoolean(args[0]); + + args.rval().setUndefined(); + return true; +} + static const char* telemetryNames[static_cast<int>(JSMetric::Count)] = { #define LIT(NAME, _) #NAME, FOR_EACH_JS_METRIC(LIT) @@ -10571,6 +10600,12 @@ JS_FN_HELP("createUserArrayBuffer", CreateUserArrayBuffer, 1, 0, "This is currently restricted to require a delay of 0 and will not accept" "any extra arguments. No return value is given and there is no clearTimeout."), + JS_FN_HELP("setCSPEnabled", SetCSPEnabled, 1, 0, +"setCSPEnabled(enabled)", +"Enable or disable Content Security Policy restrictions for eval() and Function().\n" +"When enabled (true), string compilation will be blocked. When disabled (false),\n" +"string compilation is allowed. Defaults to disabled."), + JS_FN_HELP("setPromiseRejectionTrackerCallback", SetPromiseRejectionTrackerCallback, 1, 0, "setPromiseRejectionTrackerCallback()", "Sets the callback to be invoked whenever a Promise rejection is unhandled\n"