Bug 1299483 - CSP: Implement 'strict-dynamic', parser changes. r=dveditz,freddyb
authorChristoph Kerschbaumer <ckerschb@christophkerschbaumer.com>
Tue, 08 Nov 2016 13:08:33 +0100
changeset 321519 908b79c3788827e44af80f75f99396ec7f626ecb
parent 321518 22f5124ac4b6ba1e080081ac659871d178a9c929
child 321520 d16e2f01a122cd3bf9abd9cf91d831ad53e7dfd9
push id83632
push usermozilla@christophkerschbaumer.com
push dateTue, 08 Nov 2016 15:52:17 +0000
treeherdermozilla-inbound@f3c84b104987 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdveditz, freddyb
bugs1299483
milestone52.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1299483 - CSP: Implement 'strict-dynamic', parser changes. r=dveditz,freddyb
dom/locales/en-US/chrome/security/csp.properties
dom/security/nsCSPParser.cpp
dom/security/nsCSPParser.h
modules/libpref/init/all.js
--- a/dom/locales/en-US/chrome/security/csp.properties
+++ b/dom/locales/en-US/chrome/security/csp.properties
@@ -34,16 +34,27 @@ ignoringUnknownOption = Ignoring unknown
 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):
 # %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
+# LOCALIZATION NOTE (ignoringStrictDynamic):
+# %1$S is the ignored src
+ignoringStrictDynamic = Ignoring source “%1$S” (Only supported within script-src). 
+# LOCALIZATION NOTE (strictDynamicButNoHashOrNonce):
+# %1$S is the csp directive that contains 'strict-dynamic'
+# 'strict-dynamic' should not be localized
+strictDynamicButNoHashOrNonce = Keyword ‘strict-dynamic’ within “%1$S” with no valid nonce or hash might block all scripts from loading
 # LOCALIZATION NOTE (reportURInotHttpsOrHttp2):
 # %1$S is the ETLD of the report URI that is not HTTP or HTTPS
 reportURInotHttpsOrHttp2 = The report URI (%1$S) should be an HTTP or HTTPS URI.
 # LOCALIZATION NOTE (reportURInotInReportOnlyHeader):
 # %1$S is the ETLD of the page with the policy
 reportURInotInReportOnlyHeader = This site (%1$S) has a Report-Only policy without a report URI. CSP will not block and cannot report violations of this policy.
 # LOCALIZATION NOTE (failedToParseUnrecognizedSource):
 # %1$S is the CSP Source that could not be parsed
--- a/dom/security/nsCSPParser.cpp
+++ b/dom/security/nsCSPParser.cpp
@@ -118,37 +118,40 @@ nsCSPTokenizer::tokenizeCSPPolicy(const 
   nsCSPTokenizer tokenizer(aPolicyString.BeginReading(),
                            aPolicyString.EndReading());
 
   tokenizer.generateTokens(outTokens);
 }
 
 /* ===== nsCSPParser ==================== */
 bool nsCSPParser::sCSPExperimentalEnabled = false;
+bool nsCSPParser::sStrictDynamicEnabled = false;
 
 nsCSPParser::nsCSPParser(cspTokens& aTokens,
                          nsIURI* aSelfURI,
                          nsCSPContext* aCSPContext,
                          bool aDeliveredViaMetaTag)
  : mCurChar(nullptr)
  , mEndChar(nullptr)
  , mHasHashOrNonce(false)
+ , mStrictDynamic(false)
  , mUnsafeInlineKeywordSrc(nullptr)
  , mChildSrc(nullptr)
  , mFrameSrc(nullptr)
  , mTokens(aTokens)
  , mSelfURI(aSelfURI)
  , mPolicy(nullptr)
  , mCSPContext(aCSPContext)
  , mDeliveredViaMetaTag(aDeliveredViaMetaTag)
 {
   static bool initialized = false;
   if (!initialized) {
     initialized = true;
     Preferences::AddBoolVarCache(&sCSPExperimentalEnabled, "security.csp.experimentalEnabled");
+    Preferences::AddBoolVarCache(&sStrictDynamicEnabled, "security.csp.enableStrictDynamic");
   }
   CSPPARSERLOG(("nsCSPParser::nsCSPParser"));
 }
 
 nsCSPParser::~nsCSPParser()
 {
   CSPPARSERLOG(("nsCSPParser::~nsCSPParser"));
 }
@@ -526,16 +529,32 @@ nsCSPParser::keywordSource()
                NS_ConvertUTF16toUTF8(mCurValue).get()));
 
   // Special case handling for 'self' which is not stored internally as a keyword,
   // but rather creates a nsCSPHostSrc using the selfURI
   if (CSP_IsKeyword(mCurToken, CSP_SELF)) {
     return CSP_CreateHostSrcFromURI(mSelfURI);
   }
 
+  if (CSP_IsKeyword(mCurToken, CSP_STRICT_DYNAMIC)) {
+    // make sure strict dynamic is enabled
+    if (!sStrictDynamicEnabled) {
+      return nullptr;
+    }
+    if (!CSP_IsDirective(mCurDir[0], nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE)) {
+      // Todo: Enforce 'strict-dynamic' within default-src; see Bug 1313937
+      const char16_t* params[] = { u"strict-dynamic" };
+      logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringStrictDynamic",
+                               params, ArrayLength(params));
+      return nullptr;
+    }
+    mStrictDynamic = true;
+    return new nsCSPKeywordSrc(CSP_KeywordToEnum(mCurToken));
+  }
+
   if (CSP_IsKeyword(mCurToken, CSP_UNSAFE_INLINE)) {
       nsWeakPtr ctx = mCSPContext->GetLoadingContext();
       nsCOMPtr<nsIDocument> doc = do_QueryReferent(ctx);
       if (doc) {
         doc->SetHasUnsafeInlineCSP(true);
       }
     // make sure script-src only contains 'unsafe-inline' once;
     // ignore duplicates and log warning
@@ -1182,35 +1201,68 @@ nsCSPParser::directive()
   if (CSP_IsDirective(mCurDir[0], nsIContentSecurityPolicy::SANDBOX_DIRECTIVE)) {
     sandboxFlagList(cspDir);
     return;
   }
 
   // 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;
+  mStrictDynamic = false;
   mUnsafeInlineKeywordSrc = nullptr;
 
   // Try to parse all the srcs by handing the array off to directiveValue
   nsTArray<nsCSPBaseSrc*> srcs;
   directiveValue(srcs);
 
   // If we can not parse any srcs; we let the source expression be the empty set ('none')
   // see, http://www.w3.org/TR/CSP11/#source-list-parsing
   if (srcs.Length() == 0) {
     nsCSPKeywordSrc *keyword = new nsCSPKeywordSrc(CSP_NONE);
     srcs.AppendElement(keyword);
   }
 
-  // Ignore unsafe-inline within script-src or style-src if nonce
-  // or hash is specified, see:
-  // http://www.w3.org/TR/CSP2/#directive-script-src
-  if ((cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) ||
-       cspDir->equals(nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE)) &&
-      mHasHashOrNonce && mUnsafeInlineKeywordSrc) {
+  // 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");
+    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. Please also note that
+      // nsCSPKeywordSrc() can not be invalidated and always returns false unless the
+      // keyword is 'strict-dynamic' in which case we allow the load if the script is
+      // not parser created!
+      srcs[i]->invalidate();
+      // Log a message to the console that src will be ignored.
+      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
+      if (!srcStr.EqualsASCII(CSP_EnumToKeyword(CSP_STRICT_DYNAMIC)) &&
+          !srcStr.EqualsASCII(CSP_EnumToKeyword(CSP_UNSAFE_EVAL)) &&
+          !StringBeginsWith(NS_ConvertUTF16toUTF8(srcStr), NS_LITERAL_CSTRING("'nonce-")) &&
+          !StringBeginsWith(NS_ConvertUTF16toUTF8(srcStr), NS_LITERAL_CSTRING("'sha")))
+      {
+        const char16_t* params[] = { srcStr.get() };
+        logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringSrcForStrictDynamic",
+                                 params, ArrayLength(params));
+      }
+    }
+    // Log a warning that all scripts might be blocked because the policy contains
+    // 'strict-dynamic' but no valid nonce or hash.
+    if (!mHasHashOrNonce) {
+      const char16_t* params[] = { mCurDir[0].get() };
+      logWarningErrorToConsole(nsIScriptError::warningFlag, "strictDynamicButNoHashOrNonce",
+                               params, ArrayLength(params));
+    }
+  }
+  else if (mHasHashOrNonce && mUnsafeInlineKeywordSrc &&
+           (cspDir->equals(nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE) ||
+            cspDir->equals(nsIContentSecurityPolicy::STYLE_SRC_DIRECTIVE))) {
     mUnsafeInlineKeywordSrc->invalidate();
     // log to the console that unsafe-inline will be ignored
     const char16_t* params[] = { u"'unsafe-inline'" };
     logWarningErrorToConsole(nsIScriptError::warningFlag, "ignoringSrcWithinScriptStyleSrc",
                              params, ArrayLength(params));
   }
 
   // Add the newly created srcs to the directive and add the directive to the policy
--- a/dom/security/nsCSPParser.h
+++ b/dom/security/nsCSPParser.h
@@ -107,16 +107,17 @@ class nsCSPParser {
 
   private:
     nsCSPParser(cspTokens& aTokens,
                 nsIURI* aSelfURI,
                 nsCSPContext* aCSPContext,
                 bool aDeliveredViaMetaTag);
 
     static bool sCSPExperimentalEnabled;
+    static bool sStrictDynamicEnabled;
 
     ~nsCSPParser();
 
 
     // Parsing the CSP using the source-list from http://www.w3.org/TR/CSP11/#source-list
     nsCSPPolicy*        policy();
     void                directive();
     nsCSPDirective*     directiveName();
@@ -231,18 +232,20 @@ class nsCSPParser {
  */
 
     const char16_t*    mCurChar;
     const char16_t*    mEndChar;
     nsString           mCurValue;
     nsString           mCurToken;
     nsTArray<nsString> mCurDir;
 
-    // cache variables to ignore unsafe-inline if hash or nonce is specified
+    // 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
     nsCSPKeywordSrc*   mUnsafeInlineKeywordSrc; // null, otherwise invlidate()
 
     // cache variables for child-src and frame-src directive handling.
     // frame-src is deprecated in favor of child-src, however if we
     // see a frame-src directive, it takes precedence for frames and iframes.
     // At the end of parsing, if we have a child-src directive, we need to
     // decide whether it will handle frames, or if there is a frame-src we
     // should honor instead.
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -2130,16 +2130,17 @@ pref("signed.applets.codebase_principal_
 pref("security.checkloaduri", true);
 pref("security.xpconnect.plugin.unrestricted", true);
 // security-sensitive dialogs should delay button enabling. In milliseconds.
 pref("security.dialog_enable_delay", 1000);
 pref("security.notification_enable_delay", 500);
 
 pref("security.csp.enable", true);
 pref("security.csp.experimentalEnabled", false);
+pref("security.csp.enableStrictDynamic", true);
 
 // Default Content Security Policy to apply to signed contents.
 pref("security.signed_content.CSP.default", "script-src 'self'; style-src 'self'");
 
 // Mixed content blocking
 pref("security.mixed_content.block_active_content", false);
 pref("security.mixed_content.block_display_content", false);