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 348396 908b79c3788827e44af80f75f99396ec7f626ecb
parent 348395 22f5124ac4b6ba1e080081ac659871d178a9c929
child 348397 d16e2f01a122cd3bf9abd9cf91d831ad53e7dfd9
push id10298
push userraliiev@mozilla.com
push dateMon, 14 Nov 2016 12:33:03 +0000
treeherdermozilla-aurora@7e29173b1641 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdveditz, freddyb
bugs1299483
milestone52.0a1
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);