Bug 1797070 - CSP: Add a basic implementation of unsafe-hashes behind a flag. r=freddyb

Differential Revision: https://phabricator.services.mozilla.com/D160046
This commit is contained in:
Tom Schuster 2022-11-07 17:56:23 +00:00
parent a24dca9645
commit b680625ab0
21 changed files with 83 additions and 69 deletions

View File

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

View File

@ -125,7 +125,10 @@ interface nsIContentSecurityPolicy : nsISerializable
/*
* Whether this policy allows inline script or style.
* @param aContentPolicyType Either TYPE_SCRIPT or TYPE_STYLESHEET
* @param aContentPolicyType Either SCRIPT_SRC_(ELEM|ATTR)_DIRECTIVE or
* STYLE_SRC_(ELEM|ATTR)_DIRECTIVE.
* @param aHasUnsafeHash Only hash this when the 'unsafe-hashes' directive is
* also specified.
* @param aNonce The nonce string to check against the policy
* @param aParserCreated If the script element was created by the HTML Parser
* @param aTriggeringElement The script element of the inline resource to
@ -142,6 +145,7 @@ interface nsIContentSecurityPolicy : nsISerializable
* (block the rules if false).
*/
boolean getAllowsInline(in nsIContentSecurityPolicy_CSPDirective aDirective,
in bool aHasUnsafeHash,
in AString aNonce,
in boolean aParserCreated,
in Element aTriggeringElement,

View File

@ -132,23 +132,36 @@ static nsIScriptGlobalObject* GetGlobalObject(nsIChannel* aChannel) {
}
static bool AllowedByCSP(nsIContentSecurityPolicy* aCSP,
const nsAString& aContentOfPseudoScript) {
const nsACString& aJavaScriptURL) {
if (!aCSP) {
return true;
}
// javascript: is a "navigation" type, so script-src-elem applies.
// https://w3c.github.io/webappsec-csp/#should-block-navigation-request
// Step 3. If result is "Allowed", and if navigation requests current URLs
// scheme is javascript:
//
// Step 3.1.1.2 If directives inline check returns "Allowed" when executed
// upon null, "navigation" and navigation requests current URL, skip to the
// next directive.
//
// This means /type/ is "navigation" and /source string/ is the
// "navigation requests current URL".
//
// Per
// https://w3c.github.io/webappsec-csp/#effective-directive-for-inline-check
// type "navigation" maps to the effective directive script-src-elem.
bool allowsInlineScript = true;
nsresult rv =
aCSP->GetAllowsInline(nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE,
u""_ns, // aNonce
true, // aParserCreated
nullptr, // aElement,
nullptr, // nsICSPEventListener
aContentOfPseudoScript, // aContent
0, // aLineNumber
0, // aColumnNumber
true, // aHasUnsafeHash
u""_ns, // aNonce
true, // aParserCreated
nullptr, // aElement,
nullptr, // nsICSPEventListener
NS_ConvertASCIItoUTF16(aJavaScriptURL), // aContent
0, // aLineNumber
0, // aColumnNumber
&allowsInlineScript);
return (NS_SUCCEEDED(rv) && allowsInlineScript);
@ -210,11 +223,7 @@ nsresult nsJSThunk::EvaluateScript(
// once we have determined the target document.
nsCOMPtr<nsIContentSecurityPolicy> csp = loadInfo->GetCspToInherit();
nsAutoCString script(mScript);
// Unescape the script
NS_UnescapeURL(script);
if (!AllowedByCSP(csp, NS_ConvertASCIItoUTF16(script))) {
if (!AllowedByCSP(csp, mURL)) {
return NS_ERROR_DOM_RETVAL_UNDEFINED;
}
@ -264,7 +273,7 @@ nsresult nsJSThunk::EvaluateScript(
// against if the triggering principal is system.
if (targetDoc->NodePrincipal()->Subsumes(loadInfo->TriggeringPrincipal())) {
nsCOMPtr<nsIContentSecurityPolicy> targetCSP = targetDoc->GetCsp();
if (!AllowedByCSP(targetCSP, NS_ConvertASCIItoUTF16(script))) {
if (!AllowedByCSP(targetCSP, mURL)) {
return NS_ERROR_DOM_RETVAL_UNDEFINED;
}
}
@ -305,6 +314,10 @@ nsresult nsJSThunk::EvaluateScript(
return NS_ERROR_DOM_SECURITY_ERR;
}
nsAutoCString script(mScript);
// Unescape the script
NS_UnescapeURL(script);
JS::Rooted<JS::Value> v(cx, JS::UndefinedValue());
// Finally, we have everything needed to evaluate the expression.
JS::CompileOptions options(cx);

View File

@ -801,8 +801,9 @@ static bool CSPAllowsInlineScript(nsIScriptElement* aElement,
bool allowInlineScript = false;
nsresult rv = csp->GetAllowsInline(
nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE, nonce, parserCreated,
scriptContent, nullptr /* nsICSPEventListener */, u""_ns,
nsIContentSecurityPolicy::SCRIPT_SRC_ELEM_DIRECTIVE,
false /* aHasUnsafeHash */, nonce, parserCreated, scriptContent,
nullptr /* nsICSPEventListener */, u""_ns,
aElement->GetScriptLineNumber(), aElement->GetScriptColumnNumber(),
&allowInlineScript);
return NS_SUCCEEDED(rv) && allowInlineScript;

View File

@ -572,8 +572,9 @@ void nsCSPContext::reportInlineViolation(
}
NS_IMETHODIMP
nsCSPContext::GetAllowsInline(CSPDirective aDirective, const nsAString& aNonce,
bool aParserCreated, Element* aTriggeringElement,
nsCSPContext::GetAllowsInline(CSPDirective aDirective, bool aHasUnsafeHash,
const nsAString& aNonce, bool aParserCreated,
Element* aTriggeringElement,
nsICSPEventListener* aCSPEventListener,
const nsAString& aContentOfPseudoScript,
uint32_t aLineNumber, uint32_t aColumnNumber,
@ -616,12 +617,20 @@ nsCSPContext::GetAllowsInline(CSPDirective aDirective, const nsAString& aNonce,
element->GetScriptText(content);
}
}
if (content.IsEmpty()) {
content = aContentOfPseudoScript;
}
allowed =
mPolicies[i]->allows(aDirective, CSP_HASH, content, aParserCreated);
// Per https://w3c.github.io/webappsec-csp/#match-element-to-source-list
// Step 5. If type is "script" or "style", or unsafe-hashes flag is true:
//
// aHasUnsafeHash is true for event handlers (type "script attribute"),
// style= attributes (type "style attribute") and the javascript: protocol.
if (!aHasUnsafeHash || mPolicies[i]->allows(aDirective, CSP_UNSAFE_HASHES,
u""_ns, aParserCreated)) {
allowed =
mPolicies[i]->allows(aDirective, CSP_HASH, content, aParserCreated);
}
if (!allowed) {
// policy is violoated: deny the load unless policy is report only and

View File

@ -454,6 +454,11 @@ nsCSPBaseSrc* nsCSPParser::keywordSource() {
return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken));
}
if (StaticPrefs::security_csp_unsafe_hashes_enabled() &&
CSP_IsKeyword(mCurToken, CSP_UNSAFE_HASHES)) {
return new nsCSPKeywordSrc(CSP_UTF16KeywordToEnum(mCurToken));
}
if (CSP_IsKeyword(mCurToken, CSP_UNSAFE_ALLOW_REDIRECTS)) {
if (!CSP_IsDirective(mCurDir[0],
nsIContentSecurityPolicy::NAVIGATE_TO_DIRECTIVE)) {
@ -1070,10 +1075,13 @@ void nsCSPParser::directive() {
nsAutoString srcStr;
srcs[i]->toString(srcStr);
// Even though we invalidate all of the srcs internally, we don't want to
// log messages for the srcs: (1) strict-dynamic, (2) unsafe-inline, (3)
// nonces, and (4) hashes
// log messages for the srcs: 'strict-dynamic', 'unsafe-inline',
// 'unsafe-hashes', nonces, and hashes, because those still apply even
// with 'strict-dynamic'.
// TODO the comment seems wrong 'unsafe-eval' vs 'unsafe-inline'.
if (!srcStr.EqualsASCII(CSP_EnumToUTF8Keyword(CSP_STRICT_DYNAMIC)) &&
!srcStr.EqualsASCII(CSP_EnumToUTF8Keyword(CSP_UNSAFE_EVAL)) &&
!srcStr.EqualsASCII(CSP_EnumToUTF8Keyword(CSP_UNSAFE_HASHES)) &&
!StringBeginsWith(
srcStr, nsDependentString(CSP_EnumToUTF16Keyword(CSP_NONCE))) &&
!StringBeginsWith(srcStr, u"'sha"_ns)) {

View File

@ -875,7 +875,7 @@ bool nsCSPKeywordSrc::allows(enum CSPKeyword aKeyword,
"%s",
CSP_EnumToUTF8Keyword(aKeyword),
NS_ConvertUTF16toUTF8(aHashOrNonce).get(),
mInvalidated ? "yes" : "false"));
mInvalidated ? "true" : "false"));
if (mInvalidated) {
// only 'self', 'report-sample' and 'unsafe-inline' are keywords that can be

View File

@ -116,6 +116,7 @@ inline CSPDirective CSP_StringToCSPDirective(const nsAString& aDir) {
MACRO(CSP_SELF, "'self'") \
MACRO(CSP_UNSAFE_INLINE, "'unsafe-inline'") \
MACRO(CSP_UNSAFE_EVAL, "'unsafe-eval'") \
MACRO(CSP_UNSAFE_HASHES, "'unsafe-hashes'") \
MACRO(CSP_NONE, "'none'") \
MACRO(CSP_NONCE, "'nonce-") \
MACRO(CSP_REPORT_SAMPLE, "'report-sample'") \

View File

@ -129,6 +129,7 @@ function run_test() {
let inlineOK = true;
inlineOK = csp.getAllowsInline(
Ci.nsIContentSecurityPolicy.SCRIPT_SRC_ELEM_DIRECTIVE,
false, // aHasUnsafeHash
"", // aNonce
false, // aParserCreated
null, // aTriggeringElement
@ -206,6 +207,7 @@ function run_test() {
let inlineOK = true;
inlineOK = csp.getAllowsInline(
Ci.nsIContentSecurityPolicy.SCRIPT_SRC_ELEM_DIRECTIVE,
false, // aHasUnsafeHash
"", // aNonce
false, // aParserCreated
null, // aTriggeringElement

View File

@ -306,12 +306,11 @@ bool nsStyleUtil::CSPAllowsInlineStyle(
return true;
}
nsIContentSecurityPolicy::CSPDirective directive =
nsIContentSecurityPolicy::STYLE_SRC_ATTR_DIRECTIVE;
bool isStyleElement = false;
// query the nonce
nsAutoString nonce;
if (aElement && aElement->NodeInfo()->NameAtom() == nsGkAtoms::style) {
directive = nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE;
isStyleElement = true;
nsString* cspNonce =
static_cast<nsString*>(aElement->GetProperty(nsGkAtoms::nonce));
if (cspNonce) {
@ -320,11 +319,13 @@ bool nsStyleUtil::CSPAllowsInlineStyle(
}
bool allowInlineStyle = true;
rv = csp->GetAllowsInline(directive, nonce,
false, // aParserCreated only applies to scripts
aElement, nullptr, // nsICSPEventListener
aStyleText, aLineNumber, aColumnNumber,
&allowInlineStyle);
rv = csp->GetAllowsInline(
isStyleElement ? nsIContentSecurityPolicy::STYLE_SRC_ELEM_DIRECTIVE
: nsIContentSecurityPolicy::STYLE_SRC_ATTR_DIRECTIVE,
!isStyleElement /* aHasUnsafeHash */, nonce,
false, // aParserCreated only applies to scripts
aElement, nullptr, // nsICSPEventListener
aStyleText, aLineNumber, aColumnNumber, &allowInlineStyle);
NS_ENSURE_SUCCESS(rv, false);
return allowInlineStyle;

View File

@ -12859,6 +12859,12 @@
value: true
mirror: always
# unsafe-hashes source keyword
- name: security.csp.unsafe-hashes.enabled
type: bool
value: false
mirror: always
# The script-src-attr and script-src-elem directive
- name: security.csp.script-src-attr-elem.enabled
type: bool

View File

@ -1 +1 @@
prefs: [dom.targetBlankNoOpener.enabled:false]
prefs: [dom.targetBlankNoOpener.enabled:false, security.csp.unsafe-hashes.enabled:true]

View File

@ -1,3 +0,0 @@
[javascript_src_allowed-href.html]
[javascript: navigation using <a href> should be allowed]
expected: FAIL

View File

@ -1,5 +0,0 @@
[javascript_src_allowed-href_blank-script-src-elem.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[javascript: navigation using <a href target=_blank> should be allowed]
expected: FAIL

View File

@ -1,5 +0,0 @@
[javascript_src_allowed-href_blank.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[javascript: navigation using <a href target=_blank> should be allowed]
expected: FAIL

View File

@ -1,5 +0,0 @@
[javascript_src_allowed-window_location.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[Test that the javascript: src is allowed to run]
expected: FAIL

View File

@ -1,5 +0,0 @@
[javascript_src_allowed-window_open.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
[Test that the javascript: src is allowed to run]
expected: FAIL

View File

@ -1,5 +0,0 @@
[script_event_handlers_denied_missing_unsafe_hashes.html]
expected: TIMEOUT
[Test that the inline event handler is not allowed to run]
expected: NOTRUN

View File

@ -1,6 +0,0 @@
implementation-status: backlog
[style_attribute_denied_missing_unsafe_hashes.html]
expected: TIMEOUT
[Test that the inline style attribute is blocked]
expected: NOTRUN

View File

@ -1,3 +1,4 @@
[string-compilation-nonce-classic.html]
prefs: [security.csp.unsafe-hashes.enabled:true]
expected:
if (os == "android") and fission: [OK, TIMEOUT]

View File

@ -1,3 +1,4 @@
[string-compilation-nonce-module.html]
prefs: [security.csp.unsafe-hashes.enabled:true]
expected:
if (os == "android") and fission: [OK, TIMEOUT]