From f41915445f0549fe008067812e73e78e816b7bf0 Mon Sep 17 00:00:00 2001 From: Rob Wu Date: Thu, 14 Nov 2024 03:28:09 +0000 Subject: [PATCH] Bug 1930749 - Add option to Cu.Sandbox to specify CSP r=mccr8 Differential Revision: https://phabricator.services.mozilla.com/D228711 --- caps/nsScriptSecurityManager.cpp | 5 +- js/xpconnect/idl/xpccomponents.idl | 7 ++ js/xpconnect/src/Sandbox.cpp | 99 +++++++++++----- js/xpconnect/src/xpcprivate.h | 2 + js/xpconnect/tests/unit/test_sandbox_csp.js | 110 ++++++++++++++++++ js/xpconnect/tests/unit/xpcshell.toml | 2 + .../extensions/ExtensionContent.sys.mjs | 6 + 7 files changed, 196 insertions(+), 35 deletions(-) create mode 100644 js/xpconnect/tests/unit/test_sandbox_csp.js diff --git a/caps/nsScriptSecurityManager.cpp b/caps/nsScriptSecurityManager.cpp index ee3ced1ed7c0..a2c8339beef8 100644 --- a/caps/nsScriptSecurityManager.cpp +++ b/caps/nsScriptSecurityManager.cpp @@ -497,9 +497,8 @@ bool nsScriptSecurityManager::ContentSecurityPolicyPermitsJSAction( // Get the CSP for addon sandboxes. If the principal is expanded and has a // csp, we're probably in luck. auto* basePrin = BasePrincipal::Cast(subjectPrincipal); - // ContentScriptAddonPolicy means it is also an expanded principal, thus - // this is in a sandbox used as a content script. - if (basePrin->ContentScriptAddonPolicy()) { + // TODO bug 1548468: Move CSP off ExpandedPrincipal. + if (basePrin->Is()) { basePrin->As()->GetCsp(getter_AddRefs(csp)); } // don't do anything unless there's a CSP diff --git a/js/xpconnect/idl/xpccomponents.idl b/js/xpconnect/idl/xpccomponents.idl index 0146f407b55a..650876b1bb91 100644 --- a/js/xpconnect/idl/xpccomponents.idl +++ b/js/xpconnect/idl/xpccomponents.idl @@ -185,6 +185,13 @@ interface nsIXPCComponents_Utils : nsISupports * 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 * 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 * property is optional, but very useful for tracking memory usage. A * recommended value for this property is an absolute path to the script diff --git a/js/xpconnect/src/Sandbox.cpp b/js/xpconnect/src/Sandbox.cpp index 33ed044c73e9..e6030c090d45 100644 --- a/js/xpconnect/src/Sandbox.cpp +++ b/js/xpconnect/src/Sandbox.cpp @@ -1149,42 +1149,35 @@ bool xpc::GlobalProperties::DefineInSandbox(JSContext* cx, return Define(cx, obj); } -/** - * 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) { +nsresult SetSandboxCSP(nsISupports* prinOrSop, const nsAString& cspString) { nsCOMPtr principal = do_QueryInterface(prinOrSop); if (!principal) { - return NS_OK; + return NS_ERROR_INVALID_ARG; } - auto* basePrin = BasePrincipal::Cast(principal); - // We only get an addonPolicy if the principal is an - // expanded principal with an extension principal in it. - 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; + if (!basePrin->Is()) { + return NS_ERROR_INVALID_ARG; } + auto* expanded = basePrin->As(); - nsString url; - MOZ_TRY_VAR(url, addonPolicy->GetURL(u""_ns)); - - nsCOMPtr 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(); nsCOMPtr 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 selfURI; + MOZ_TRY(NS_NewURI(getter_AddRefs(selfURI), "moz-extension://dummy"_ns)); + #ifdef MOZ_DEBUG // Bug 1548468: Move CSP off ExpandedPrincipal expanded->GetCsp(getter_AddRefs(csp)); @@ -1196,7 +1189,7 @@ nsresult ApplyAddonContentScriptCSP(nsISupports* prinOrSop) { nsAutoString parsedPolicyStr; for (uint32_t i = 0; i < count; i++) { csp->GetPolicyString(i, parsedPolicyStr); - MOZ_ASSERT(!parsedPolicyStr.Equals(baseCSP)); + MOZ_ASSERT(!parsedPolicyStr.Equals(cspString)); } } } @@ -1217,7 +1210,7 @@ nsresult ApplyAddonContentScriptCSP(nsISupports* prinOrSop) { MOZ_TRY( csp->SetRequestContextWithPrincipal(clonedPrincipal, selfURI, ""_ns, 0)); - MOZ_TRY(csp->AppendPolicy(baseCSP, false, false)); + MOZ_TRY(csp->AppendPolicy(cspString, false, false)); expanded->SetCsp(csp); return NS_OK; @@ -1798,6 +1791,33 @@ bool OptionsBase::ParseString(const char* name, nsString& prop) { return true; } +/* + * Helper that tries to get a string property from the options object. + */ +bool OptionsBase::ParseOptionalString(const char* name, Maybe& 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. */ @@ -1883,6 +1903,8 @@ bool SandboxOptions::Parse() { ParseBoolean("isWebExtensionContentScript", &isWebExtensionContentScript) && ParseBoolean("forceSecureContext", &forceSecureContext) && + ParseOptionalString("sandboxContentSecurityPolicy", + sandboxContentSecurityPolicy) && ParseString("sandboxName", sandboxName) && ParseObject("sameZoneAs", &sameZoneAs) && ParseBoolean("freshCompartment", &freshCompartment) && @@ -1994,8 +2016,6 @@ nsresult nsXPCComponents_utils_Sandbox::CallOrConstruct( } else { ok = GetExpandedPrincipal(cx, obj, options, getter_AddRefs(expanded)); prinOrSop = expanded; - // If this is an addon content script we need to apply the csp. - MOZ_TRY(ApplyAddonContentScriptCSP(prinOrSop)); } } else { ok = GetPrincipalOrSOP(cx, obj, getter_AddRefs(prinOrSop)); @@ -2010,6 +2030,21 @@ nsresult nsXPCComponents_utils_Sandbox::CallOrConstruct( 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))) { return ThrowAndFail(NS_ERROR_INVALID_ARG, cx, _retval); } diff --git a/js/xpconnect/src/xpcprivate.h b/js/xpconnect/src/xpcprivate.h index 41ba17e71355..c35440064d8b 100644 --- a/js/xpconnect/src/xpcprivate.h +++ b/js/xpconnect/src/xpcprivate.h @@ -2269,6 +2269,7 @@ class MOZ_STACK_CLASS OptionsBase { bool ParseJSString(const char* name, JS::MutableHandleString prop); bool ParseString(const char* name, nsCString& prop); bool ParseString(const char* name, nsString& prop); + bool ParseOptionalString(const char* name, mozilla::Maybe& prop); bool ParseId(const char* name, JS::MutableHandleId id); bool ParseUInt32(const char* name, uint32_t* prop); @@ -2306,6 +2307,7 @@ class MOZ_STACK_CLASS SandboxOptions : public OptionsBase { bool wantExportHelpers; bool isWebExtensionContentScript; JS::RootedObject proto; + mozilla::Maybe sandboxContentSecurityPolicy; nsCString sandboxName; JS::RootedObject sameZoneAs; bool forceSecureContext; diff --git a/js/xpconnect/tests/unit/test_sandbox_csp.js b/js/xpconnect/tests/unit/test_sandbox_csp.js new file mode 100644 index 000000000000..408760ffc767 --- /dev/null +++ b/js/xpconnect/tests/unit/test_sandbox_csp.js @@ -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" + ); +}); diff --git a/js/xpconnect/tests/unit/xpcshell.toml b/js/xpconnect/tests/unit/xpcshell.toml index 711246b6e347..ca08db783f31 100644 --- a/js/xpconnect/tests/unit/xpcshell.toml +++ b/js/xpconnect/tests/unit/xpcshell.toml @@ -361,6 +361,8 @@ head = "head_ongc.js" ["test_sandbox_atob.js"] +["test_sandbox_csp.js"] + ["test_sandbox_metadata.js"] ["test_sandbox_name.js"] diff --git a/toolkit/components/extensions/ExtensionContent.sys.mjs b/toolkit/components/extensions/ExtensionContent.sys.mjs index 6763a5201719..0625fa3b694e 100644 --- a/toolkit/components/extensions/ExtensionContent.sys.mjs +++ b/toolkit/components/extensions/ExtensionContent.sys.mjs @@ -958,6 +958,7 @@ class ContentScriptContextChild extends BaseContext { let isMV2 = extension.manifestVersion == 2; let wantGlobalProperties; + let sandboxContentSecurityPolicy; if (isMV2) { // In MV2, fetch/XHR support cross-origin requests. // WebSocket was also included to avoid CSP effects (bug 1676024). @@ -965,11 +966,16 @@ class ContentScriptContextChild extends BaseContext { } else { // In MV3, fetch/XHR have the same capabilities as the web page. 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, { metadata, sandboxName: `Content Script ${extension.policy.debugName}`, sandboxPrototype: contentWindow, + sandboxContentSecurityPolicy, sameZoneAs: contentWindow, wantXrays: true, isWebExtensionContentScript: true,