Skip to content

Commit 0ccab1a

Browse files
Bug 1941780 - Add JSShell test for Frame.eval bypassCSP option r=arai
Differential Revision: https://phabricator.services.mozilla.com/D274097
1 parent 55970ee commit 0ccab1a

File tree

2 files changed

+190
-1
lines changed

2 files changed

+190
-1
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Test that debugger evaluation can bypass CSP restrictions when bypassCSP option is set.
2+
3+
var g = newGlobal({newCompartment: true});
4+
var dbg = new Debugger(g);
5+
6+
// Create a function to trigger a debugger statement before enabling CSP.
7+
g.eval("function triggerDebugger() { debugger; }");
8+
9+
// Create functions that will use eval/new Function and which will be called
10+
// from Frame.eval.
11+
g.eval("function triggerEval() { return eval('2 + 2'); }");
12+
g.eval("function triggerNewFunction() { return new Function('return 2 + 2')(); }");
13+
14+
const EXPECTED_VALUE = 4;
15+
function assertSuccess(expression, options = {}) {
16+
let evaluated = false;
17+
dbg.onDebuggerStatement = function (frame) {
18+
assertEq(frame.eval(expression, options).return, EXPECTED_VALUE);
19+
evaluated = true;
20+
};
21+
g.triggerDebugger();
22+
assertEq(evaluated, true);
23+
}
24+
25+
function assertThrows(expression, options = {}) {
26+
let evaluated = false;
27+
dbg.onDebuggerStatement = function (frame) {
28+
assertEq(frame.eval(expression, options).throw !== undefined, true);
29+
evaluated = true;
30+
};
31+
g.triggerDebugger();
32+
assertEq(evaluated, true);
33+
}
34+
35+
// Smoke test without enabling CSP
36+
37+
// evaluation without eval/new Function always succeeds
38+
assertSuccess("2 + 2");
39+
assertSuccess("2 + 2", { bypassCSP: false });
40+
assertSuccess("2 + 2", { bypassCSP: true });
41+
42+
// Default value for bypassCSP
43+
assertSuccess("eval('2 + 2')");
44+
assertSuccess("new Function('return 2 + 2')()");
45+
assertSuccess("triggerEval()");
46+
assertSuccess("triggerNewFunction()");
47+
48+
// bypassCSP=false
49+
assertSuccess("eval('2 + 2')", { bypassCSP: false });
50+
assertSuccess("new Function('return 2 + 2')()", { bypassCSP: false });
51+
assertSuccess("triggerEval()", { bypassCSP: false });
52+
assertSuccess("triggerNewFunction()", { bypassCSP: false });
53+
54+
// bypassCSP=true
55+
assertSuccess("eval('2 + 2')", { bypassCSP: true });
56+
assertSuccess("new Function('return 2 + 2')()", { bypassCSP: true });
57+
assertSuccess("triggerEval()", { bypassCSP: true });
58+
assertSuccess("triggerNewFunction()", { bypassCSP: true });
59+
60+
// Enable CSP
61+
setCSPEnabled(true);
62+
63+
// evaluation without eval/new Function always succeeds
64+
assertSuccess("2 + 2");
65+
assertSuccess("2 + 2", { bypassCSP: false });
66+
assertSuccess("2 + 2", { bypassCSP: true });
67+
68+
// Default value for bypassCSP, should all fail
69+
assertThrows("eval('2 + 2')");
70+
assertThrows("new Function('return 2 + 2')()");
71+
assertThrows("triggerEval()");
72+
assertThrows("triggerNewFunction()");
73+
74+
// bypassCSP=false, should all fail
75+
assertThrows("eval('2 + 2')", { bypassCSP: false });
76+
assertThrows("new Function('return 2 + 2')()", { bypassCSP: false });
77+
assertThrows("triggerEval()", { bypassCSP: false });
78+
assertThrows("triggerNewFunction()", { bypassCSP: false });
79+
80+
// bypassCSP=true, should all succeed
81+
assertSuccess("eval('2 + 2')", { bypassCSP: true });
82+
assertSuccess("new Function('return 2 + 2')()", { bypassCSP: true });
83+
assertSuccess("triggerEval()", { bypassCSP: true });
84+
assertSuccess("triggerNewFunction()", { bypassCSP: true });
85+
86+
// Define a few functions using eval with and without bypassCSP.
87+
// They should both behave the same when triggered from a later Frame.eval, and
88+
// only the bypassCSP flag for the later Frame.eval should matter.
89+
dbg.onDebuggerStatement = function (frame) {
90+
frame.eval("globalThis.fnWithBypass = () => eval('2 + 2')", { bypassCSP: true });
91+
frame.eval("globalThis.fnWithoutBypass = () => eval('2 + 2')", { bypassCSP: false });
92+
};
93+
g.triggerDebugger();
94+
95+
// Call the function defined with bypassCSP=true, should only work when
96+
// bypassCSP is true for the current Frame.eval.
97+
assertThrows("globalThis.fnWithBypass()");
98+
assertThrows("globalThis.fnWithBypass()", { bypassCSP: false });
99+
assertSuccess("globalThis.fnWithBypass()", { bypassCSP: true });
100+
101+
// Call the function defined with bypassCSP=false, should only work when
102+
// bypassCSP is true for the current Frame.eval.
103+
assertThrows("globalThis.fnWithoutBypass()");
104+
assertThrows("globalThis.fnWithoutBypass()", { bypassCSP: false });
105+
assertSuccess("globalThis.fnWithoutBypass()", { bypassCSP: true });
106+
107+
// Test for async frames.
108+
const asyncEvalExpression = `(async function() {
109+
try {
110+
globalThis.evalBeforeAwaitSuccess = false;
111+
globalThis.evalAfterAwaitSuccess = false;
112+
eval("1 + 1");
113+
globalThis.evalBeforeAwaitSuccess = true;
114+
await 1;
115+
eval("2 + 2");
116+
globalThis.evalAfterAwaitSuccess = true;
117+
} catch {}
118+
})()`;
119+
120+
// Test async code with bypassCSP=true, only the eval before the await should be
121+
// successful.
122+
dbg.onDebuggerStatement = function (frame) {
123+
frame.onPop = completion => {
124+
frame.eval(asyncEvalExpression, { bypassCSP: true });
125+
dbg.removeDebuggee(g); // avoid the DebuggeeWouldRun exception
126+
drainJobQueue();
127+
dbg.addDebuggee(g);
128+
}
129+
};
130+
g.triggerDebugger();
131+
132+
dbg.onDebuggerStatement = function (frame) {
133+
assertEq(frame.eval("globalThis.evalBeforeAwaitSuccess", options).return, true);
134+
assertEq(frame.eval("globalThis.evalAfterAwaitSuccess", options).return, false);
135+
};
136+
g.triggerDebugger();
137+
138+
// Test async code with bypassCSP=false, all eval should fail.
139+
dbg.onDebuggerStatement = function (frame) {
140+
frame.onPop = completion => {
141+
frame.eval(asyncEvalExpression, { bypassCSP: false });
142+
dbg.removeDebuggee(g); // avoid the DebuggeeWouldRun exception
143+
drainJobQueue();
144+
dbg.addDebuggee(g);
145+
}
146+
};
147+
g.triggerDebugger();
148+
drainJobQueue();
149+
150+
dbg.onDebuggerStatement = function (frame) {
151+
assertEq(frame.eval("globalThis.evalBeforeAwaitSuccess", options).return, false);
152+
assertEq(frame.eval("globalThis.evalAfterAwaitSuccess", options).return, false);
153+
};
154+
g.triggerDebugger();

js/src/shell/js.cpp

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -960,8 +960,23 @@ class ShellPrincipals final : public JSPrincipals {
960960
static ShellPrincipals fullyTrusted;
961961
};
962962

963+
// Global CSP state for the shell. When true, CSP restrictions are enforced.
964+
static bool gCSPEnabled = false;
965+
966+
static bool ContentSecurityPolicyAllows(
967+
JSContext* cx, JS::RuntimeCode kind, JS::Handle<JSString*> codeString,
968+
JS::CompilationType compilationType,
969+
JS::Handle<JS::StackGCVector<JSString*>> parameterStrings,
970+
JS::Handle<JSString*> bodyString,
971+
JS::Handle<JS::StackGCVector<JS::Value>> parameterArgs,
972+
JS::Handle<JS::Value> bodyArg, bool* outCanCompileStrings) {
973+
// If CSP is enabled, block string compilation.
974+
*outCanCompileStrings = !gCSPEnabled;
975+
return true;
976+
}
977+
963978
JSSecurityCallbacks ShellPrincipals::securityCallbacks = {
964-
nullptr, // contentSecurityPolicyAllows
979+
ContentSecurityPolicyAllows,
965980
nullptr, // codeForEvalGets
966981
subsumes};
967982

@@ -1654,6 +1669,20 @@ static bool SetTimeout(JSContext* cx, unsigned argc, Value* vp) {
16541669
return true;
16551670
}
16561671

1672+
static bool SetCSPEnabled(JSContext* cx, unsigned argc, Value* vp) {
1673+
CallArgs args = CallArgsFromVp(argc, vp);
1674+
1675+
if (args.length() != 1) {
1676+
JS_ReportErrorASCII(cx, "setCSPEnabled() requires one boolean argument");
1677+
return false;
1678+
}
1679+
1680+
gCSPEnabled = JS::ToBoolean(args[0]);
1681+
1682+
args.rval().setUndefined();
1683+
return true;
1684+
}
1685+
16571686
static const char* telemetryNames[static_cast<int>(JSMetric::Count)] = {
16581687
#define LIT(NAME, _) #NAME,
16591688
FOR_EACH_JS_METRIC(LIT)
@@ -10571,6 +10600,12 @@ JS_FN_HELP("createUserArrayBuffer", CreateUserArrayBuffer, 1, 0,
1057110600
"This is currently restricted to require a delay of 0 and will not accept"
1057210601
"any extra arguments. No return value is given and there is no clearTimeout."),
1057310602

10603+
JS_FN_HELP("setCSPEnabled", SetCSPEnabled, 1, 0,
10604+
"setCSPEnabled(enabled)",
10605+
"Enable or disable Content Security Policy restrictions for eval() and Function().\n"
10606+
"When enabled (true), string compilation will be blocked. When disabled (false),\n"
10607+
"string compilation is allowed. Defaults to disabled."),
10608+
1057410609
JS_FN_HELP("setPromiseRejectionTrackerCallback", SetPromiseRejectionTrackerCallback, 1, 0,
1057510610
"setPromiseRejectionTrackerCallback()",
1057610611
"Sets the callback to be invoked whenever a Promise rejection is unhandled\n"

0 commit comments

Comments
 (0)