Bug 1930749 - Add option to Cu.Sandbox to specify CSP r=mccr8

Differential Revision: https://phabricator.services.mozilla.com/D228711
This commit is contained in:
Rob Wu 2024-11-14 03:28:09 +00:00
parent 4e69784010
commit f41915445f
7 changed files with 196 additions and 35 deletions

View File

@ -497,9 +497,8 @@ bool nsScriptSecurityManager::ContentSecurityPolicyPermitsJSAction(
// Get the CSP for addon sandboxes. If the principal is expanded and has a // Get the CSP for addon sandboxes. If the principal is expanded and has a
// csp, we're probably in luck. // csp, we're probably in luck.
auto* basePrin = BasePrincipal::Cast(subjectPrincipal); auto* basePrin = BasePrincipal::Cast(subjectPrincipal);
// ContentScriptAddonPolicy means it is also an expanded principal, thus // TODO bug 1548468: Move CSP off ExpandedPrincipal.
// this is in a sandbox used as a content script. if (basePrin->Is<ExpandedPrincipal>()) {
if (basePrin->ContentScriptAddonPolicy()) {
basePrin->As<ExpandedPrincipal>()->GetCsp(getter_AddRefs(csp)); basePrin->As<ExpandedPrincipal>()->GetCsp(getter_AddRefs(csp));
} }
// don't do anything unless there's a CSP // don't do anything unless there's a CSP

View File

@ -185,6 +185,13 @@ interface nsIXPCComponents_Utils : nsISupports
* Content scripts should pass the window they're running in as this * Content scripts should pass the window they're running in as this
* parameter, in order to ensure that the script is cleaned up at the * parameter, in order to ensure that the script is cleaned up at the
* same time as the content itself. * same time as the content itself.
* - sandboxContentSecurityPolicy: {String} The Content Security Policy
* to apply in this sandbox. This can be used to restrict eval
* (e.g. "script-src 'self'"). It does not apply to DOM methods
* that were retrieved from objects outside the sandbox.
* This is only implemented for Expanded Principals; if desired for
* other principals, bug 1548468 must be resolved first.
* When not specified, the default CSP is used (usually no CSP).
* - sandboxName: {String} Identifies the sandbox in about:memory. This * - sandboxName: {String} Identifies the sandbox in about:memory. This
* property is optional, but very useful for tracking memory usage. A * property is optional, but very useful for tracking memory usage. A
* recommended value for this property is an absolute path to the script * recommended value for this property is an absolute path to the script

View File

@ -1149,42 +1149,35 @@ bool xpc::GlobalProperties::DefineInSandbox(JSContext* cx,
return Define(cx, obj); return Define(cx, obj);
} }
/** nsresult SetSandboxCSP(nsISupports* prinOrSop, const nsAString& cspString) {
* If enabled, apply the extension base CSP, then apply the
* content script CSP which will either be a default or one
* provided by the extension in its manifest.
*/
nsresult ApplyAddonContentScriptCSP(nsISupports* prinOrSop) {
nsCOMPtr<nsIPrincipal> principal = do_QueryInterface(prinOrSop); nsCOMPtr<nsIPrincipal> principal = do_QueryInterface(prinOrSop);
if (!principal) { if (!principal) {
return NS_OK; return NS_ERROR_INVALID_ARG;
} }
auto* basePrin = BasePrincipal::Cast(principal); auto* basePrin = BasePrincipal::Cast(principal);
// We only get an addonPolicy if the principal is an if (!basePrin->Is<ExpandedPrincipal>()) {
// expanded principal with an extension principal in it. return NS_ERROR_INVALID_ARG;
auto* addonPolicy = basePrin->ContentScriptAddonPolicy();
if (!addonPolicy) {
return NS_OK;
}
// For backwards compatibility, content scripts have no CSP
// in manifest v2. Only apply content script CSP to V3 or later.
if (addonPolicy->ManifestVersion() < 3) {
return NS_OK;
} }
auto* expanded = basePrin->As<ExpandedPrincipal>();
nsString url;
MOZ_TRY_VAR(url, addonPolicy->GetURL(u""_ns));
nsCOMPtr<nsIURI> selfURI;
MOZ_TRY(NS_NewURI(getter_AddRefs(selfURI), url));
const nsAString& baseCSP = addonPolicy->BaseCSP();
// If we got here, we're definitly an expanded principal.
auto expanded = basePrin->As<ExpandedPrincipal>();
nsCOMPtr<nsIContentSecurityPolicy> csp; nsCOMPtr<nsIContentSecurityPolicy> csp;
// The choice of self-uri (self-origin) to use in a Sandbox depends on the
// use case. For now, we default to a non-existing URL because there is no
// use case that requires 'self' to have a particular value. Consumers can
// always specify the URL explicitly instead of 'self'. Besides, the CSP
// enforcement in a Sandbox is barely implemented, except for eval()-like
// execution.
//
// moz-extension:-resources are never blocked by CSP because the protocol is
// registered with URI_IS_LOCAL_RESOURCE and subjectToCSP in nsCSPService.cpp
// therefore allows the load. This matches the CSP spec, which explicitly
// states that CSP should not interfere with addons. Because of this, we do
// not need to set selfURI to the real moz-extension:-URL, even in cases
// where we want to restrict all scripts except for moz-extension:-URLs.
nsCOMPtr<nsIURI> selfURI;
MOZ_TRY(NS_NewURI(getter_AddRefs(selfURI), "moz-extension://dummy"_ns));
#ifdef MOZ_DEBUG #ifdef MOZ_DEBUG
// Bug 1548468: Move CSP off ExpandedPrincipal // Bug 1548468: Move CSP off ExpandedPrincipal
expanded->GetCsp(getter_AddRefs(csp)); expanded->GetCsp(getter_AddRefs(csp));
@ -1196,7 +1189,7 @@ nsresult ApplyAddonContentScriptCSP(nsISupports* prinOrSop) {
nsAutoString parsedPolicyStr; nsAutoString parsedPolicyStr;
for (uint32_t i = 0; i < count; i++) { for (uint32_t i = 0; i < count; i++) {
csp->GetPolicyString(i, parsedPolicyStr); csp->GetPolicyString(i, parsedPolicyStr);
MOZ_ASSERT(!parsedPolicyStr.Equals(baseCSP)); MOZ_ASSERT(!parsedPolicyStr.Equals(cspString));
} }
} }
} }
@ -1217,7 +1210,7 @@ nsresult ApplyAddonContentScriptCSP(nsISupports* prinOrSop) {
MOZ_TRY( MOZ_TRY(
csp->SetRequestContextWithPrincipal(clonedPrincipal, selfURI, ""_ns, 0)); csp->SetRequestContextWithPrincipal(clonedPrincipal, selfURI, ""_ns, 0));
MOZ_TRY(csp->AppendPolicy(baseCSP, false, false)); MOZ_TRY(csp->AppendPolicy(cspString, false, false));
expanded->SetCsp(csp); expanded->SetCsp(csp);
return NS_OK; return NS_OK;
@ -1798,6 +1791,33 @@ bool OptionsBase::ParseString(const char* name, nsString& prop) {
return true; return true;
} }
/*
* Helper that tries to get a string property from the options object.
*/
bool OptionsBase::ParseOptionalString(const char* name, Maybe<nsString>& prop) {
RootedValue value(mCx);
bool found;
bool ok = ParseValue(name, &value, &found);
NS_ENSURE_TRUE(ok, false);
if (!found || value.isUndefined()) {
return true;
}
if (!value.isString()) {
JS_ReportErrorASCII(mCx, "Expected a string value for property %s", name);
return false;
}
nsAutoJSString strVal;
if (!strVal.init(mCx, value.toString())) {
return false;
}
prop = Some(strVal);
return true;
}
/* /*
* Helper that tries to get jsid property from the options object. * Helper that tries to get jsid property from the options object.
*/ */
@ -1883,6 +1903,8 @@ bool SandboxOptions::Parse() {
ParseBoolean("isWebExtensionContentScript", ParseBoolean("isWebExtensionContentScript",
&isWebExtensionContentScript) && &isWebExtensionContentScript) &&
ParseBoolean("forceSecureContext", &forceSecureContext) && ParseBoolean("forceSecureContext", &forceSecureContext) &&
ParseOptionalString("sandboxContentSecurityPolicy",
sandboxContentSecurityPolicy) &&
ParseString("sandboxName", sandboxName) && ParseString("sandboxName", sandboxName) &&
ParseObject("sameZoneAs", &sameZoneAs) && ParseObject("sameZoneAs", &sameZoneAs) &&
ParseBoolean("freshCompartment", &freshCompartment) && ParseBoolean("freshCompartment", &freshCompartment) &&
@ -1994,8 +2016,6 @@ nsresult nsXPCComponents_utils_Sandbox::CallOrConstruct(
} else { } else {
ok = GetExpandedPrincipal(cx, obj, options, getter_AddRefs(expanded)); ok = GetExpandedPrincipal(cx, obj, options, getter_AddRefs(expanded));
prinOrSop = expanded; prinOrSop = expanded;
// If this is an addon content script we need to apply the csp.
MOZ_TRY(ApplyAddonContentScriptCSP(prinOrSop));
} }
} else { } else {
ok = GetPrincipalOrSOP(cx, obj, getter_AddRefs(prinOrSop)); ok = GetPrincipalOrSOP(cx, obj, getter_AddRefs(prinOrSop));
@ -2010,6 +2030,21 @@ nsresult nsXPCComponents_utils_Sandbox::CallOrConstruct(
return ThrowAndFail(NS_ERROR_INVALID_ARG, cx, _retval); return ThrowAndFail(NS_ERROR_INVALID_ARG, cx, _retval);
} }
if (options.sandboxContentSecurityPolicy.isSome()) {
if (!expanded) {
// CSP is currently stored on ExpandedPrincipal. If CSP moves off
// ExpandedPrincipal (bug 1548468), then we can drop/relax this check.
JS_ReportErrorASCII(cx,
"sandboxContentSecurityPolicy is currently only "
"supported with ExpandedPrincipals");
return ThrowAndFail(NS_ERROR_INVALID_ARG, cx, _retval);
}
rv = SetSandboxCSP(prinOrSop, options.sandboxContentSecurityPolicy.value());
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
if (NS_FAILED(AssembleSandboxMemoryReporterName(cx, options.sandboxName))) { if (NS_FAILED(AssembleSandboxMemoryReporterName(cx, options.sandboxName))) {
return ThrowAndFail(NS_ERROR_INVALID_ARG, cx, _retval); return ThrowAndFail(NS_ERROR_INVALID_ARG, cx, _retval);
} }

View File

@ -2269,6 +2269,7 @@ class MOZ_STACK_CLASS OptionsBase {
bool ParseJSString(const char* name, JS::MutableHandleString prop); bool ParseJSString(const char* name, JS::MutableHandleString prop);
bool ParseString(const char* name, nsCString& prop); bool ParseString(const char* name, nsCString& prop);
bool ParseString(const char* name, nsString& prop); bool ParseString(const char* name, nsString& prop);
bool ParseOptionalString(const char* name, mozilla::Maybe<nsString>& prop);
bool ParseId(const char* name, JS::MutableHandleId id); bool ParseId(const char* name, JS::MutableHandleId id);
bool ParseUInt32(const char* name, uint32_t* prop); bool ParseUInt32(const char* name, uint32_t* prop);
@ -2306,6 +2307,7 @@ class MOZ_STACK_CLASS SandboxOptions : public OptionsBase {
bool wantExportHelpers; bool wantExportHelpers;
bool isWebExtensionContentScript; bool isWebExtensionContentScript;
JS::RootedObject proto; JS::RootedObject proto;
mozilla::Maybe<nsString> sandboxContentSecurityPolicy;
nsCString sandboxName; nsCString sandboxName;
JS::RootedObject sameZoneAs; JS::RootedObject sameZoneAs;
bool forceSecureContext; bool forceSecureContext;

View File

@ -0,0 +1,110 @@
"use strict";
function isEvalAllowed(sandbox) {
try {
Cu.evalInSandbox("eval('1234')", sandbox);
return true;
} catch (e) {
Assert.equal(e.message, "call to eval() blocked by CSP", "Eval error msg");
return false;
}
}
add_task(function test_empty_csp() {
let sand = Cu.Sandbox(["http://example.com/"], {
sandboxContentSecurityPolicy: "",
});
Assert.ok(isEvalAllowed(sand), "eval() not blocked with empty CSP string");
});
add_task(function test_undefined_csp() {
let sand = Cu.Sandbox(["http://example.com/"], {
sandboxContentSecurityPolicy: undefined,
});
Assert.ok(isEvalAllowed(sand), "eval() not blocked with undefined CSP");
});
add_task(function test_malformed_csp() {
let sand = Cu.Sandbox(["http://example.com/"], {
sandboxContentSecurityPolicy: "This is not a valid CSP value",
});
Assert.ok(isEvalAllowed(sand), "eval() not blocked with undefined CSP");
});
add_task(function test_allowed_by_sandboxContentSecurityPolicy() {
let sand = Cu.Sandbox(["http://example.com/"], {
sandboxContentSecurityPolicy: "script-src 'unsafe-eval';",
});
Assert.ok(isEvalAllowed(sand), "eval() allowed by 'unsafe-eval' CSP");
});
add_task(function test_blocked_by_sandboxContentSecurityPolicy() {
let sand = Cu.Sandbox(["http://example.com/"], {
sandboxContentSecurityPolicy: "script-src 'none';",
});
// Until bug 1548468 is fixed, CSP only works with an ExpandedPrincipal.
Assert.ok(Cu.getObjectPrincipal(sand).isExpandedPrincipal, "Exp principal");
Assert.ok(!isEvalAllowed(sand), "eval() should be blocked by CSP");
// sandbox.eval is also blocked: callers should use Cu.evalInSandbox instead.
Assert.throws(
() => sand.eval("123"),
/EvalError: call to eval\(\) blocked by CSP/,
"sandbox.eval() is also blocked by CSP"
);
});
add_task(function test_sandboxContentSecurityPolicy_on_content_principal() {
Assert.throws(
() => {
Cu.Sandbox("http://example.com", {
sandboxContentSecurityPolicy: "script-src 'none';",
});
},
/Error: sandboxContentSecurityPolicy is currently only supported with ExpandedPrincipals/,
// Until bug 1548468 is fixed, CSP only works with an ExpandedPrincipal.
"sandboxContentSecurityPolicy does not work with content principal"
);
});
add_task(function test_sandboxContentSecurityPolicy_on_null_principal() {
Assert.throws(
() => {
Cu.Sandbox(null, { sandboxContentSecurityPolicy: "script-src 'none';" });
},
/Error: sandboxContentSecurityPolicy is currently only supported with ExpandedPrincipals/,
// Until bug 1548468 is fixed, CSP only works with an ExpandedPrincipal.
"sandboxContentSecurityPolicy does not work with content principal"
);
});
add_task(function test_sandboxContentSecurityPolicy_on_content_principal() {
Assert.throws(
() => {
Cu.Sandbox("http://example.com", {
sandboxContentSecurityPolicy: "script-src 'none';",
});
},
/Error: sandboxContentSecurityPolicy is currently only supported with ExpandedPrincipals/,
// Until bug 1548468 is fixed, CSP only works with an ExpandedPrincipal.
"sandboxContentSecurityPolicy does not work with content principal"
);
});
add_task(function test_sandboxContentSecurityPolicy_on_system_principal() {
const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
// Note: if we ever introduce support for CSP in non-Expanded principals,
// then the test should set security.allow_eval_with_system_principal=true
// to make sure that eval() is blocked because of CSP and not another reason.
Assert.throws(
() => {
Cu.Sandbox(systemPrincipal, {
sandboxContentSecurityPolicy: "script-src 'none';",
});
},
/Error: sandboxContentSecurityPolicy is currently only supported with ExpandedPrincipals/,
// Until bug 1548468 is fixed, CSP only works with an ExpandedPrincipal.
"sandboxContentSecurityPolicy does not work with system principal"
);
});

View File

@ -361,6 +361,8 @@ head = "head_ongc.js"
["test_sandbox_atob.js"] ["test_sandbox_atob.js"]
["test_sandbox_csp.js"]
["test_sandbox_metadata.js"] ["test_sandbox_metadata.js"]
["test_sandbox_name.js"] ["test_sandbox_name.js"]

View File

@ -958,6 +958,7 @@ class ContentScriptContextChild extends BaseContext {
let isMV2 = extension.manifestVersion == 2; let isMV2 = extension.manifestVersion == 2;
let wantGlobalProperties; let wantGlobalProperties;
let sandboxContentSecurityPolicy;
if (isMV2) { if (isMV2) {
// In MV2, fetch/XHR support cross-origin requests. // In MV2, fetch/XHR support cross-origin requests.
// WebSocket was also included to avoid CSP effects (bug 1676024). // WebSocket was also included to avoid CSP effects (bug 1676024).
@ -965,11 +966,16 @@ class ContentScriptContextChild extends BaseContext {
} else { } else {
// In MV3, fetch/XHR have the same capabilities as the web page. // In MV3, fetch/XHR have the same capabilities as the web page.
wantGlobalProperties = []; wantGlobalProperties = [];
// In MV3, the base CSP is enforced for content scripts. Overrides are
// currently not supported, but this was considered at some point, see
// https://bugzilla.mozilla.org/show_bug.cgi?id=1581611#c10
sandboxContentSecurityPolicy = extension.policy.baseCSP;
} }
this.sandbox = Cu.Sandbox(principal, { this.sandbox = Cu.Sandbox(principal, {
metadata, metadata,
sandboxName: `Content Script ${extension.policy.debugName}`, sandboxName: `Content Script ${extension.policy.debugName}`,
sandboxPrototype: contentWindow, sandboxPrototype: contentWindow,
sandboxContentSecurityPolicy,
sameZoneAs: contentWindow, sameZoneAs: contentWindow,
wantXrays: true, wantXrays: true,
isWebExtensionContentScript: true, isWebExtensionContentScript: true,