Index: extensions/common/csp_validator.cc |
diff --git a/extensions/common/csp_validator.cc b/extensions/common/csp_validator.cc |
index e6c789f6f8e59fce0d2afdc732efac15d82a37f6..2b62d68b36f44d6becb49d4e1f23c8b947b27bbd 100644 |
--- a/extensions/common/csp_validator.cc |
+++ b/extensions/common/csp_validator.cc |
@@ -28,12 +28,21 @@ namespace { |
const char kDefaultSrc[] = "default-src"; |
const char kScriptSrc[] = "script-src"; |
const char kObjectSrc[] = "object-src"; |
+const char kFrameSrc[] = "frame-src"; |
+const char kChildSrc[] = "child-src"; |
+ |
+const char kDirectiveSeparator[] = ";"; |
+ |
const char kPluginTypes[] = "plugin-types"; |
const char kObjectSrcDefaultDirective[] = "object-src 'self';"; |
const char kScriptSrcDefaultDirective[] = |
"script-src 'self' chrome-extension-resource:;"; |
+const char kAppSandboxSubframeSrcDefaultDirective[] = "child-src 'self';"; |
+const char kAppSandboxScriptSrcDefaultDirective[] = |
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval';"; |
+ |
const char kSandboxDirectiveName[] = "sandbox"; |
const char kAllowSameOriginToken[] = "allow-same-origin"; |
const char kAllowTopNavigation[] = "allow-top-navigation"; |
@@ -55,12 +64,33 @@ const char* const kHashSourcePrefixes[] = { |
"'sha512-" |
}; |
-struct DirectiveStatus { |
- explicit DirectiveStatus(const char* name) |
- : directive_name(name), seen_in_policy(false) {} |
+class DirectiveStatus { |
+ public: |
+ DirectiveStatus(const char* name) { directive_names_.push_back(name); } |
+ // Subframe related directives can have multiple directive names: "child-src" |
+ // or "frame-src". |
+ DirectiveStatus(const char* name1, const char* name2) { |
+ directive_names_.push_back(name1); |
+ directive_names_.push_back(name2); |
+ } |
+ bool Matches(const std::string& directive_name) const { |
+ for (const auto& directive : directive_names_) { |
+ if (directive_name == directive) |
+ return true; |
+ } |
+ return false; |
+ } |
+ bool seen_in_policy() const { return seen_in_policy_; } |
+ void set_seen_in_policy() { seen_in_policy_ = true; } |
- const char* directive_name; |
- bool seen_in_policy; |
+ std::string name() const { |
+ DCHECK(!directive_names_.empty()); |
+ return directive_names_[0]; |
+ } |
+ |
+ private: |
+ std::vector<const char*> directive_names_; |
+ bool seen_in_policy_ = false; |
}; |
// Returns whether |url| starts with |scheme_and_separator| and does not have a |
@@ -199,29 +229,6 @@ void GetSecureDirectiveValues(const std::string& directive_name, |
sane_csp_parts->back().push_back(';'); |
} |
-// Returns true if |directive_name| matches |status.directive_name|. |
-bool UpdateStatus(const std::string& directive_name, |
- base::StringTokenizer* tokenizer, |
- DirectiveStatus* status, |
- int options, |
- std::vector<std::string>* sane_csp_parts, |
- std::vector<InstallWarning>* warnings) { |
- if (directive_name != status->directive_name) |
- return false; |
- |
- if (!status->seen_in_policy) { |
- status->seen_in_policy = true; |
- GetSecureDirectiveValues(directive_name, tokenizer, options, sane_csp_parts, |
- warnings); |
- } else { |
- // Don't show any errors for duplicate CSP directives, because it will be |
- // ignored by the CSP parser (http://www.w3.org/TR/CSP2/#policy-parsing). |
- GetSecureDirectiveValues(directive_name, tokenizer, options, sane_csp_parts, |
- NULL); |
- } |
- return true; |
-} |
- |
// Returns true if the |plugin_type| is one of the fully sandboxed plugin types. |
bool PluginTypeAllowed(const std::string& plugin_type) { |
for (size_t i = 0; i < arraysize(kSandboxedPluginTypes); ++i) { |
@@ -259,82 +266,229 @@ bool AllowedToHaveInsecureObjectSrc( |
return false; |
} |
-} // namespace |
- |
-bool ContentSecurityPolicyIsLegal(const std::string& policy) { |
- // We block these characters to prevent HTTP header injection when |
- // representing the content security policy as an HTTP header. |
- const char kBadChars[] = {',', '\r', '\n', '\0'}; |
- |
- return policy.find_first_of(kBadChars, 0, arraysize(kBadChars)) == |
- std::string::npos; |
-} |
+class CSPEnforcer { |
+ public: |
+ CSPEnforcer(bool show_missing_csp_warnings) |
+ : show_missing_csp_warnings_(show_missing_csp_warnings) {} |
+ virtual ~CSPEnforcer() {} |
+ |
+ std::string Enforce(const std::string& policy, |
+ std::vector<InstallWarning>* warnings); |
+ |
+ protected: |
+ virtual std::string GetDefaultCSPValue(const DirectiveStatus& status) = 0; |
+ |
+ // Updates the status of a directive |directive_status| with given information |
+ // about a directive token. The directive token has name |directive_name| and |
+ // its values are in |tokenizer|. |
+ // |
+ // Returns true if |directive_status| matches |directive_name|. |
+ virtual bool VisitAndEnforce(const std::string& directive_name, |
+ DirectiveStatus* directive_status, |
+ base::StringTokenizer* tokenizer, |
+ std::vector<InstallWarning>* warnings) = 0; |
+ |
+ std::vector<DirectiveStatus> directives_; |
+ std::vector<std::string> enforced_csp_parts_; |
+ const bool show_missing_csp_warnings_; |
+}; |
-std::string SanitizeContentSecurityPolicy( |
- const std::string& policy, |
- int options, |
- std::vector<InstallWarning>* warnings) { |
- // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. |
- std::vector<std::string> directives = base::SplitString( |
- policy, ";", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); |
+std::string CSPEnforcer::Enforce(const std::string& policy, |
+ std::vector<InstallWarning>* warnings) { |
Devlin
2016/12/15 00:04:11
As discussed briefly offline, I still find this pr
lazyboy
2016/12/15 08:02:35
I think there are too many cases to consider here
|
+ enforced_csp_parts_.clear(); |
+ // If any directive that we care about isn't explicitly listed in |policy|, |
+ // "default-src" fallback is used. |
DirectiveStatus default_src_status(kDefaultSrc); |
- DirectiveStatus script_src_status(kScriptSrc); |
- DirectiveStatus object_src_status(kObjectSrc); |
- |
- bool allow_insecure_object_src = |
- AllowedToHaveInsecureObjectSrc(options, directives); |
- |
- std::vector<std::string> sane_csp_parts; |
std::vector<InstallWarning> default_src_csp_warnings; |
- for (size_t i = 0; i < directives.size(); ++i) { |
- std::string& input = directives[i]; |
- base::StringTokenizer tokenizer(input, " \t\r\n"); |
+ |
+ // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. |
+ for (const std::string& directive : base::SplitString( |
+ policy, ";", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)) { |
+ base::StringTokenizer tokenizer(directive, " \t\r\n"); |
if (!tokenizer.GetNext()) |
continue; |
std::string directive_name = base::ToLowerASCII(tokenizer.token_piece()); |
- if (UpdateStatus(directive_name, &tokenizer, &default_src_status, options, |
- &sane_csp_parts, &default_src_csp_warnings)) |
- continue; |
- if (UpdateStatus(directive_name, &tokenizer, &script_src_status, options, |
- &sane_csp_parts, warnings)) |
+ bool matches_enforcing_directive = false; |
+ for (auto& directive_status : directives_) { |
+ if (VisitAndEnforce(directive_name, &directive_status, &tokenizer, |
+ warnings)) { |
+ matches_enforcing_directive = true; |
+ break; |
+ } |
+ } |
+ if (matches_enforcing_directive) |
continue; |
- if (!allow_insecure_object_src && |
- UpdateStatus(directive_name, &tokenizer, &object_src_status, options, |
- &sane_csp_parts, warnings)) |
+ |
+ if (VisitAndEnforce(directive_name, &default_src_status, &tokenizer, |
+ &default_src_csp_warnings)) { |
continue; |
+ } |
- // Pass the other CSP directives as-is without further validation. |
- sane_csp_parts.push_back(input + ";"); |
+ // Keep this directive as is. |
+ enforced_csp_parts_.push_back(directive + kDirectiveSeparator); |
} |
- if (default_src_status.seen_in_policy) { |
- if (!script_src_status.seen_in_policy || |
- !object_src_status.seen_in_policy) { |
- // Insecure values in default-src are only relevant if either script-src |
- // or object-src is omitted. |
- if (warnings) |
- warnings->insert(warnings->end(), |
- default_src_csp_warnings.begin(), |
- default_src_csp_warnings.end()); |
+ if (default_src_status.seen_in_policy()) { |
+ for (const DirectiveStatus& directive_status : directives_) { |
+ if (!directive_status.seen_in_policy()) { |
+ // This |directive_status| falls back to "default-src". So warnings from |
+ // "default-src" will apply. |
+ if (warnings) { |
+ warnings->insert(warnings->end(), default_src_csp_warnings.begin(), |
+ default_src_csp_warnings.end()); |
+ } |
+ break; |
+ } |
} |
} else { |
- if (!script_src_status.seen_in_policy) { |
- sane_csp_parts.push_back(kScriptSrcDefaultDirective); |
- if (warnings) |
+ // Did not see "default-src". |
+ // Make sure we cover all sources from |directives_|. |
+ for (const DirectiveStatus& directive_status : directives_) { |
+ if (directive_status.seen_in_policy()) // Already covered. |
+ continue; |
+ enforced_csp_parts_.push_back(GetDefaultCSPValue(directive_status)); |
+ |
+ if (warnings && show_missing_csp_warnings_) { |
warnings->push_back(CSPInstallWarning(ErrorUtils::FormatErrorMessage( |
- manifest_errors::kInvalidCSPMissingSecureSrc, kScriptSrc))); |
+ manifest_errors::kInvalidCSPMissingSecureSrc, |
+ directive_status.name()))); |
+ } |
} |
- if (!object_src_status.seen_in_policy && !allow_insecure_object_src) { |
- sane_csp_parts.push_back(kObjectSrcDefaultDirective); |
- if (warnings) |
+ } |
+ |
+ return base::JoinString(enforced_csp_parts_, " "); |
+} |
+ |
+class ExtensionCSPEnforcer : public CSPEnforcer { |
+ public: |
+ ExtensionCSPEnforcer(bool allow_insecure_object_src, int options) |
+ : CSPEnforcer(true), options_(options) { |
+ directives_.push_back(DirectiveStatus(kScriptSrc)); |
+ if (!allow_insecure_object_src) |
+ directives_.push_back(DirectiveStatus(kObjectSrc)); |
+ } |
+ |
+ protected: |
+ bool VisitAndEnforce(const std::string& directive_name, |
+ DirectiveStatus* directive_status, |
+ base::StringTokenizer* tokenizer, |
+ std::vector<InstallWarning>* warnings) override { |
+ if (!directive_status->Matches(directive_name)) |
+ return false; |
+ |
+ if (!directive_status->seen_in_policy()) { |
+ directive_status->set_seen_in_policy(); |
+ GetSecureDirectiveValues(directive_name, tokenizer, options_, |
+ &enforced_csp_parts_, warnings); |
+ } else { |
+ // Don't show any errors for duplicate CSP directives, because it will be |
+ // ignored by the CSP parser (http://www.w3.org/TR/CSP2/#policy-parsing). |
+ GetSecureDirectiveValues(directive_name, tokenizer, options_, |
+ &enforced_csp_parts_, nullptr); |
+ } |
+ return true; |
+ } |
+ |
+ std::string GetDefaultCSPValue(const DirectiveStatus& status) override { |
+ if (status.Matches(kObjectSrc)) |
+ return kObjectSrcDefaultDirective; |
+ DCHECK(status.Matches(kScriptSrc)); |
+ return kScriptSrcDefaultDirective; |
+ } |
+ |
+ private: |
+ const int options_; |
+}; |
+ |
+class AppSandboxPageCSPEnforcer : public CSPEnforcer { |
+ public: |
+ AppSandboxPageCSPEnforcer() : CSPEnforcer(false) { |
+ directives_.push_back(DirectiveStatus(kChildSrc, kFrameSrc)); |
+ directives_.push_back(DirectiveStatus(kScriptSrc)); |
+ } |
+ |
+ protected: |
+ bool VisitAndEnforce(const std::string& directive_name, |
+ DirectiveStatus* directive_status, |
+ base::StringTokenizer* tokenizer, |
+ std::vector<InstallWarning>* warnings) override { |
+ if (!directive_status->Matches(directive_name)) |
+ return false; |
+ |
+ // Don't show any errors for duplicate CSP directives, because it will be |
+ // ignored by the CSP parser (http://www.w3.org/TR/CSP2/#policy-parsing). |
+ bool is_duplicate_directive = directive_status->seen_in_policy(); |
+ directive_status->set_seen_in_policy(); |
+ |
+ enforced_csp_parts_.push_back(directive_name); |
+ bool seen_self_or_none = false; |
+ while (tokenizer->GetNext()) { |
+ std::string source_literal = tokenizer->token(); |
+ std::string source_lower = base::ToLowerASCII(source_literal); |
+ |
+ // Keyword directive sources are surrounded with quotes, e.g. 'self', |
+ // 'sha256-...', 'unsafe-eval', 'nonce-...'. These do not specify a remote |
+ // host or '*', so keep them and restrict the rest. |
+ if (source_lower.size() > 1u && source_lower[0] == '\'' && |
+ source_lower[source_lower.size() - 1] == '\'') { |
+ if (source_lower == "'none'" || source_lower == "'self'") |
+ seen_self_or_none |= true; |
+ enforced_csp_parts_.push_back(source_lower); |
+ } else if (warnings && !is_duplicate_directive) { |
warnings->push_back(CSPInstallWarning(ErrorUtils::FormatErrorMessage( |
- manifest_errors::kInvalidCSPMissingSecureSrc, kObjectSrc))); |
+ manifest_errors::kInvalidCSPInsecureValue, source_literal, |
+ directive_name))); |
+ } |
} |
+ |
+ if (!seen_self_or_none) |
+ enforced_csp_parts_.push_back("'self'"); |
+ |
+ // Add ";" at the end of the directive. |
+ enforced_csp_parts_.back().append(kDirectiveSeparator); |
+ return true; |
} |
- return base::JoinString(sane_csp_parts, " "); |
+ std::string GetDefaultCSPValue(const DirectiveStatus& status) override { |
+ if (status.Matches(kChildSrc)) |
+ return kAppSandboxSubframeSrcDefaultDirective; |
+ DCHECK(status.Matches(kScriptSrc)); |
+ return kAppSandboxScriptSrcDefaultDirective; |
+ } |
+}; |
+ |
+} // namespace |
+ |
+bool ContentSecurityPolicyIsLegal(const std::string& policy) { |
+ // We block these characters to prevent HTTP header injection when |
+ // representing the content security policy as an HTTP header. |
+ const char kBadChars[] = {',', '\r', '\n', '\0'}; |
+ |
+ return policy.find_first_of(kBadChars, 0, arraysize(kBadChars)) == |
+ std::string::npos; |
+} |
+ |
+std::string SanitizeContentSecurityPolicy( |
+ const std::string& policy, |
+ int options, |
+ std::vector<InstallWarning>* warnings) { |
+ // See http://www.w3.org/TR/CSP/#parse-a-csp-policy for parsing algorithm. |
+ std::vector<std::string> directives = base::SplitString( |
+ policy, kDirectiveSeparator, base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); |
+ |
+ bool allow_insecure_object_src = |
+ AllowedToHaveInsecureObjectSrc(options, directives); |
+ |
+ ExtensionCSPEnforcer csp_enforcer(allow_insecure_object_src, options); |
+ return csp_enforcer.Enforce(policy, warnings); |
+} |
+ |
+std::string GetEffectiveSandoxedPageCSP(const std::string& policy, |
+ std::vector<InstallWarning>* warnings) { |
+ AppSandboxPageCSPEnforcer csp_enforcer; |
+ return csp_enforcer.Enforce(policy, warnings); |
} |
bool ContentSecurityPolicyIsSandboxed( |