Bug 1529337 - Implement CSP 'script-src-elem' and 'script-src-attr' directives. r=freddyb,webidl,smaug

Differential Revision: https://phabricator.services.mozilla.com/D150965
This commit is contained in:
Tom Schuster 2022-07-13 10:21:48 +00:00
parent e2cffdf906
commit e8d99d0ef6
24 changed files with 127 additions and 78 deletions

View File

@ -1043,7 +1043,7 @@ nsresult EventListenerManager::SetEventHandler(nsAtom* aName,
if (csp) {
bool allowsInlineScript = true;
rv = csp->GetAllowsInline(
nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE,
nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE,
u""_ns, // aNonce
true, // aParserCreated (true because attribute event handler)
aElement,

View File

@ -62,6 +62,8 @@ interface nsIContentSecurityPolicy : nsISerializable
SANDBOX_DIRECTIVE = 19,
WORKER_SRC_DIRECTIVE = 20,
NAVIGATE_TO_DIRECTIVE = 21,
SCRIPT_SRC_ELEM_DIRECTIVE = 22,
SCRIPT_SRC_ATTR_DIRECTIVE = 23,
};
/**

View File

@ -137,9 +137,11 @@ static bool AllowedByCSP(nsIContentSecurityPolicy* aCSP,
return true;
}
// javascript: is a "navigation" type, so script-src-elem applies.
// https://w3c.github.io/webappsec-csp/#effective-directive-for-inline-check
bool allowsInlineScript = true;
nsresult rv =
aCSP->GetAllowsInline(nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE,
aCSP->GetAllowsInline(nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE,
u""_ns, // aNonce
true, // aParserCreated
nullptr, // aElement,

View File

@ -35,17 +35,22 @@ ignoringDuplicateSrc = Ignoring duplicate source %1$S
# LOCALIZATION NOTE (ignoringSrcFromMetaCSP):
# %1$S defines the ignored src
ignoringSrcFromMetaCSP = Ignoring source %1$S (Not supported when delivered via meta element).
# LOCALIZATION NOTE (ignoringSrcWithinScriptStyleSrc):
# LOCALIZATION NOTE (ignoringSrcWithinNonceOrHashDirective):
# %1$S is the ignored src (e.g. "unsafe-inline")
# %2$S is the directive (e.g. "script-src-elem")
ignoringSrcWithinNonceOrHashDirective = Ignoring “%1$S” within %2$S: nonce-source or hash-source specified
# LOCALIZATION NOTE (ignoringScriptSrcForStrictDynamic):
# %1$S is the ignored src
# script-src and style-src are directive names and should not be localized
ignoringSrcWithinScriptStyleSrc = Ignoring “%1$S” within script-src or style-src: nonce-source or hash-source specified
# LOCALIZATION NOTE (ignoringSrcForStrictDynamic):
# %1$S is the ignored src
# script-src, as well as 'strict-dynamic' should not be localized
ignoringSrcForStrictDynamic = Ignoring “%1$S” within script-src: strict-dynamic specified
# %1$S is the directive src (e.g. "script-src-elem")
# 'strict-dynamic' should not be localized
ignoringScriptSrcForStrictDynamic = Ignoring “%1$S” within %2$S: strict-dynamic specified
# LOCALIZATION NOTE (ignoringStrictDynamic):
# %1$S is the ignored src
ignoringStrictDynamic = Ignoring source “%1$S” (Only supported within script-src).
# LOCALIZATION NOTE (ignoringUnsafeEval):
# %1$S is the csp directive (e.g. script-src-elem)
# 'unsafe-eval' and 'wasm-unsafe-eval' should not be localized
ignoringUnsafeEval = Ignoring unsafe-eval or wasm-unsafe-eval inside “%1$S”.
# LOCALIZATION NOTE (strictDynamicButNoHashOrNonce):
# %1$S is the csp directive that contains 'strict-dynamic'
# 'strict-dynamic' should not be localized

View File

@ -788,8 +788,6 @@ bool ScriptLoader::PreloadURIComparator::Equals(const PreloadInfo& aPi,
static bool CSPAllowsInlineScript(nsIScriptElement* aElement,
Document* aDocument) {
nsCOMPtr<nsIContentSecurityPolicy> csp = aDocument->GetCsp();
nsresult rv = NS_OK;
if (!csp) {
// no CSP --> allow
return true;
@ -810,8 +808,8 @@ static bool CSPAllowsInlineScript(nsIScriptElement* aElement,
aElement->GetParserCreated() != mozilla::dom::NOT_FROM_PARSER;
bool allowInlineScript = false;
rv = csp->GetAllowsInline(
nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE, nonce, parserCreated,
nsresult rv = csp->GetAllowsInline(
nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE, nonce, parserCreated,
scriptContent, nullptr /* nsICSPEventListener */, u""_ns,
aElement->GetScriptLineNumber(), aElement->GetScriptColumnNumber(),
&allowInlineScript);

View File

@ -518,13 +518,15 @@ void nsCSPContext::reportInlineViolation(
// let's report the hash error; no need to report the unsafe-inline error
// anymore.
if (!aNonce.IsEmpty()) {
observerSubject = (aDirective == SCRIPT_SRC_DIRECTIVE)
observerSubject = (aDirective == SCRIPT_SRC_ELEM_DIRECTIVE ||
aDirective == SCRIPT_SRC_ATTR_DIRECTIVE)
? NS_LITERAL_STRING_FROM_CSTRING(
SCRIPT_NONCE_VIOLATION_OBSERVER_TOPIC)
: NS_LITERAL_STRING_FROM_CSTRING(
STYLE_NONCE_VIOLATION_OBSERVER_TOPIC);
} else {
observerSubject = (aDirective == SCRIPT_SRC_DIRECTIVE)
observerSubject = (aDirective == SCRIPT_SRC_ELEM_DIRECTIVE ||
aDirective == SCRIPT_SRC_ATTR_DIRECTIVE)
? NS_LITERAL_STRING_FROM_CSTRING(
SCRIPT_HASH_VIOLATION_OBSERVER_TOPIC)
: NS_LITERAL_STRING_FROM_CSTRING(
@ -570,8 +572,11 @@ nsCSPContext::GetAllowsInline(CSPDirective aDirective, const nsAString& aNonce,
bool* outAllowsInline) {
*outAllowsInline = true;
if (aDirective != SCRIPT_SRC_DIRECTIVE && aDirective != STYLE_SRC_DIRECTIVE) {
MOZ_ASSERT(false, "can only allow inline for script or style");
if (aDirective != SCRIPT_SRC_ELEM_DIRECTIVE &&
aDirective != SCRIPT_SRC_ATTR_DIRECTIVE &&
aDirective != STYLE_SRC_DIRECTIVE) {
MOZ_ASSERT(false,
"can only allow inline for script-src-(attr/elem) or style");
return NS_OK;
}

View File

@ -43,6 +43,7 @@ nsCSPParser::nsCSPParser(policyTokens& aTokens, nsIURI* aSelfURI,
: mCurChar(nullptr),
mEndChar(nullptr),
mHasHashOrNonce(false),
mHasAnyUnsafeEval(false),
mStrictDynamic(false),
mUnsafeInlineKeywordSrc(nullptr),
mChildSrc(nullptr),
@ -400,7 +401,11 @@ nsCSPBaseSrc* nsCSPParser::keywordSource() {
if (CSP_IsKeyword(mCurToken, CSP_STRICT_DYNAMIC)) {
if (!CSP_IsDirective(mCurDir[0],
nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE)) {
nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) &&
!CSP_IsDirective(mCurDir[0],
nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) &&
!CSP_IsDirective(mCurDir[0],
nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE)) {
// Todo: Enforce 'strict-dynamic' within default-src; see Bug 1313937
AutoTArray<nsString, 1> params = {u"strict-dynamic"_ns};
logWarningErrorToConsole(nsIScriptError::warningFlag,
@ -438,11 +443,13 @@ nsCSPBaseSrc* nsCSPParser::keywordSource() {
if (doc) {
doc->SetHasUnsafeEvalCSP(true);
}
mHasAnyUnsafeEval = true;
return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken));
}
if (StaticPrefs::security_csp_wasm_unsafe_eval_enabled() &&
CSP_IsKeyword(mCurToken, CSP_WASM_UNSAFE_EVAL)) {
mHasAnyUnsafeEval = true;
return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken));
}
@ -917,7 +924,8 @@ nsCSPDirective* nsCSPParser::directiveName() {
}
// if we have a script-src, cache it as a fallback for worker-src
// in case child-src is not present
// in case child-src is not present. It is also used as a fallback for
// script-src-elem and script-src-attr.
if (directive == nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) {
mScriptSrc = new nsCSPScriptSrcDirective(directive);
return mScriptSrc;
@ -1003,6 +1011,7 @@ void nsCSPParser::directive() {
// make sure to reset cache variables when trying to invalidate unsafe-inline;
// unsafe-inline might not only appear in script-src, but also in default-src
mHasHashOrNonce = false;
mHasAnyUnsafeEval = false;
mStrictDynamic = false;
mUnsafeInlineKeywordSrc = nullptr;
@ -1022,8 +1031,12 @@ void nsCSPParser::directive() {
// If policy contains 'strict-dynamic' invalidate all srcs within script-src.
if (mStrictDynamic) {
MOZ_ASSERT(cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE),
"strict-dynamic only allowed within script-src");
MOZ_ASSERT(
cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) ||
cspDir->equals(
nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) ||
cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE),
"strict-dynamic only allowed within script-src(-elem|attr)");
for (uint32_t i = 0; i < srcs.Length(); i++) {
// Please note that nsCSPNonceSrc as well as nsCSPHashSrc overwrite
// invalidate(), so it's fine to just call invalidate() on all srcs.
@ -1042,9 +1055,9 @@ void nsCSPParser::directive() {
!StringBeginsWith(
srcStr, nsDependentString(CSP_EnumToUTF16Keyword(CSP_NONCE))) &&
!StringBeginsWith(srcStr, u"'sha"_ns)) {
AutoTArray<nsString, 1> params = {srcStr};
AutoTArray<nsString, 2> params = {srcStr, mCurDir[0]};
logWarningErrorToConsole(nsIScriptError::warningFlag,
"ignoringSrcForStrictDynamic", params);
"ignoringScriptSrcForStrictDynamic", params);
}
}
// Log a warning that all scripts might be blocked because the policy
@ -1056,12 +1069,26 @@ void nsCSPParser::directive() {
}
} else if (mHasHashOrNonce && mUnsafeInlineKeywordSrc &&
(cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) ||
cspDir->equals(
nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) ||
cspDir->equals(
nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE) ||
cspDir->equals(nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE))) {
mUnsafeInlineKeywordSrc->invalidate();
// log to the console that unsafe-inline will be ignored
AutoTArray<nsString, 1> params = {u"'unsafe-inline'"_ns};
// Log to the console that unsafe-inline will be ignored.
AutoTArray<nsString, 2> params = {u"'unsafe-inline'"_ns, mCurDir[0]};
logWarningErrorToConsole(nsIScriptError::warningFlag,
"ignoringSrcWithinScriptStyleSrc", params);
"ignoringSrcWithinNonceOrHashDirective", params);
}
if (mHasAnyUnsafeEval &&
(cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) ||
cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE))) {
// Log to the console that (wasm-)unsafe-eval will be ignored.
AutoTArray<nsString, 1> params = {mCurDir[0]};
logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringUnsafeEval",
params);
}
// Add the newly created srcs to the directive and add the directive to the
@ -1097,12 +1124,27 @@ nsCSPPolicy* nsCSPParser::policy() {
mChildSrc->setRestrictWorkers();
}
}
// if script-src is specified, but not worker-src and also no child-src, then
// script-src has to govern workers.
if (mScriptSrc && !mWorkerSrc && !mChildSrc) {
mScriptSrc->setRestrictWorkers();
}
// If script-src is specified and script-src-elem is not specified, then
// script-src has to govern script requests and script blocks.
if (mScriptSrc && !mPolicy->hasDirective(
nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE)) {
mScriptSrc->setRestrictScriptElem();
}
// If script-src is specified and script-src-attr is not specified, then
// script-src has to govern script attr (event handlers).
if (mScriptSrc && !mPolicy->hasDirective(
nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE)) {
mScriptSrc->setRestrictScriptAttr();
}
return mPolicy;
}

View File

@ -182,8 +182,9 @@ class nsCSPParser {
// helpers to allow invalidation of srcs within script-src and style-src
// if either 'strict-dynamic' or at least a hash or nonce is present.
bool mHasHashOrNonce; // false, if no hash or nonce is defined
bool mStrictDynamic; // false, if 'strict-dynamic' is not defined
bool mHasHashOrNonce; // false, if no hash or nonce is defined
bool mHasAnyUnsafeEval; // false, if no (wasm-)unsafe-eval keyword is used.
bool mStrictDynamic; // false, if 'strict-dynamic' is not defined
nsCSPKeywordSrc* mUnsafeInlineKeywordSrc; // null, otherwise invlidate()
// cache variables for child-src, frame-src and worker-src handling;

View File

@ -255,9 +255,11 @@ void CSP_LogLocalizedStr(const char* aName, const nsTArray<nsString>& aParams,
}
/* ===== Helpers ============================ */
// This implements
// https://w3c.github.io/webappsec-csp/#effective-directive-for-a-request.
// However the spec doesn't currently cover all request destinations, which
// we roughly represent using nsContentPolicyType.
CSPDirective CSP_ContentTypeToDirective(nsContentPolicyType aType) {
// We need to know if this is a worker so child-src can handle that case
// correctly.
switch (aType) {
case nsIContentPolicy::TYPE_IMAGE:
case nsIContentPolicy::TYPE_IMAGESET:
@ -278,7 +280,11 @@ CSPDirective CSP_ContentTypeToDirective(nsContentPolicyType aType) {
case nsIContentPolicy::TYPE_INTERNAL_PAINTWORKLET:
case nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT:
case nsIContentPolicy::TYPE_INTERNAL_FRAME_MESSAGEMANAGER_SCRIPT:
return nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE;
// (https://github.com/w3c/webappsec-csp/issues/554)
// Some of these types are not explicitly defined in the spec.
//
// Chrome seems to use script-src-elem for worklet!
return nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE;
case nsIContentPolicy::TYPE_STYLESHEET:
case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET:
@ -1210,6 +1216,16 @@ void nsCSPDirective::toDomCSPStruct(mozilla::dom::CSP& outCSP) const {
outCSP.mWorker_src.Value() = std::move(srcs);
return;
case nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE:
outCSP.mScript_src_elem.Construct();
outCSP.mScript_src_elem.Value() = std::move(srcs);
return;
case nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE:
outCSP.mScript_src_attr.Construct();
outCSP.mScript_src_attr.Value() = std::move(srcs);
return;
default:
NS_ASSERTION(false, "cannot find directive to convert CSP to JSON");
}
@ -1277,7 +1293,10 @@ bool nsCSPChildSrcDirective::equals(CSPDirective aDirective) const {
/* =============== nsCSPScriptSrcDirective ============= */
nsCSPScriptSrcDirective::nsCSPScriptSrcDirective(CSPDirective aDirective)
: nsCSPDirective(aDirective), mRestrictWorkers(false) {}
: nsCSPDirective(aDirective),
mRestrictWorkers(false),
mRestrictScriptElem(false),
mRestrictScriptAttr(false) {}
nsCSPScriptSrcDirective::~nsCSPScriptSrcDirective() = default;
@ -1285,6 +1304,12 @@ bool nsCSPScriptSrcDirective::equals(CSPDirective aDirective) const {
if (aDirective == nsIContentSecurityPolicy::WORKER_SRC_DIRECTIVE) {
return mRestrictWorkers;
}
if (aDirective == nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE) {
return mRestrictScriptElem;
}
if (aDirective == nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE) {
return mRestrictScriptAttr;
}
return (mDirective == aDirective);
}

View File

@ -88,7 +88,9 @@ static const char* CSPStrDirectives[] = {
"block-all-mixed-content", // BLOCK_ALL_MIXED_CONTENT
"sandbox", // SANDBOX_DIRECTIVE
"worker-src", // WORKER_SRC_DIRECTIVE
"navigate-to" // NAVIGATE_TO_DIRECTIVE
"navigate-to", // NAVIGATE_TO_DIRECTIVE
"script-src-elem", // SCRIPT_SRC_ELEM_DIRECTIVE
"script-src-attr", // SCRIPT_SRC_ATTR_DIRECTIVE
};
inline const char* CSP_CSPDirectiveToString(CSPDirective aDir) {
@ -510,11 +512,15 @@ class nsCSPScriptSrcDirective : public nsCSPDirective {
virtual ~nsCSPScriptSrcDirective();
void setRestrictWorkers() { mRestrictWorkers = true; }
void setRestrictScriptElem() { mRestrictScriptElem = true; }
void setRestrictScriptAttr() { mRestrictScriptAttr = true; }
virtual bool equals(CSPDirective aDirective) const override;
private:
bool mRestrictWorkers;
bool mRestrictScriptElem;
bool mRestrictScriptAttr;
};
/* =============== nsBlockAllMixedContentDirective === */

View File

@ -121,7 +121,7 @@ function run_test() {
makeTest(0, { "blocked-uri": "inline" }, false, function(csp) {
let inlineOK = true;
inlineOK = csp.getAllowsInline(
Ci.nsIContentSecurityPolicy.SCRIPT_SRC_DIRECTIVE,
Ci.nsIContentSecurityPolicy.SCRIPT_SRC_ELEM_DIRECTIVE,
"", // aNonce
false, // aParserCreated
null, // aTriggeringElement
@ -193,7 +193,7 @@ function run_test() {
makeTest(3, { "blocked-uri": "inline" }, true, function(csp) {
let inlineOK = true;
inlineOK = csp.getAllowsInline(
Ci.nsIContentSecurityPolicy.SCRIPT_SRC_DIRECTIVE,
Ci.nsIContentSecurityPolicy.SCRIPT_SRC_ELEM_DIRECTIVE,
"", // aNonce
false, // aParserCreated
null, // aTriggeringElement

View File

@ -30,6 +30,8 @@ dictionary CSP {
sequence<DOMString> block-all-mixed-content;
sequence<DOMString> sandbox;
sequence<DOMString> worker-src;
sequence<DOMString> script-src-elem;
sequence<DOMString> script-src-attr;
};
[GenerateToJSON]

View File

@ -1 +0,0 @@
implementation-status: backlog

View File

@ -1,4 +0,0 @@
[script-src-attr-allowed-src-blocked.html]
[Should not fire a security policy violation event]
expected: FAIL

View File

@ -1,5 +0,0 @@
[script-src-attr-blocked-src-allowed.html]
expected: TIMEOUT
[Should fire a security policy violation event]
expected: NOTRUN

View File

@ -1,5 +0,0 @@
[script-src-elem-allowed-attr-blocked.html]
expected: TIMEOUT
[Should fire a security policy violation for the attribute]
expected: NOTRUN

View File

@ -1,4 +0,0 @@
[script-src-elem-allowed-src-blocked.html]
[Should not fire a security policy violation event]
expected: FAIL

View File

@ -1,5 +0,0 @@
[script-src-elem-blocked-attr-allowed.html]
expected: TIMEOUT
[Should fire a security policy violation for the attribute]
expected: NOTRUN

View File

@ -1,5 +0,0 @@
[script-src-elem-blocked-src-allowed.html]
expected: TIMEOUT
[Should fire a spv event]
expected: NOTRUN

View File

@ -1,4 +0,0 @@
[strict-dynamic-elem-allowed-src-blocked.html]
[Should not fire a security policy violation event]
expected: FAIL

View File

@ -1,4 +0,0 @@
[strict-dynamic-elem-blocked-src-allowed.sub.html]
[Should fire a security policy violation event]
expected: FAIL

View File

@ -0,0 +1,4 @@
[javascript_src_allowed-href_blank-script-src-elem.html]
[javascript: navigation using <a href target=_blank> should be allowed]
expected: FAIL

View File

@ -1,3 +0,0 @@
[javascript_src_denied_missing_unsafe_hashes-href_blank-script-src-elem.html]
[javascript: navigation using <a href target=_blank> should be refused due to missing unsafe-hashes]
expected: FAIL

View File

@ -1,3 +0,0 @@
[javascript_src_denied_wrong_hash-href_blank-script-src-elem.html]
[javascript: navigation using <a href target=_blank> should be refused due to wrong hash]
expected: FAIL