| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/platform/loader/subresource_integrity.h" |
| |
| #include "base/stl_util.h" |
| #include "services/network/public/mojom/fetch_api.mojom-blink.h" |
| #include "third_party/blink/public/platform/web_crypto.h" |
| #include "third_party/blink/public/platform/web_crypto_algorithm.h" |
| #include "third_party/blink/renderer/platform/crypto.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource.h" |
| #include "third_party/blink/renderer/platform/weborigin/kurl.h" |
| #include "third_party/blink/renderer/platform/weborigin/security_origin.h" |
| #include "third_party/blink/renderer/platform/wtf/text/ascii_ctype.h" |
| #include "third_party/blink/renderer/platform/wtf/text/base64.h" |
| #include "third_party/blink/renderer/platform/wtf/text/parsing_utilities.h" |
| #include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h" |
| #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" |
| #include "third_party/blink/renderer/platform/wtf/vector.h" |
| |
| namespace blink { |
| |
| // FIXME: This should probably use common functions with ContentSecurityPolicy. |
| static bool IsIntegrityCharacter(UChar c) { |
| // Check if it's a base64 encoded value. We're pretty loose here, as there's |
| // not much risk in it, and it'll make it simpler for developers. |
| return IsASCIIAlphanumeric(c) || c == '_' || c == '-' || c == '+' || |
| c == '/' || c == '='; |
| } |
| |
| static bool IsValueCharacter(UChar c) { |
| // VCHAR per https://tools.ietf.org/html/rfc5234#appendix-B.1 |
| return c >= 0x21 && c <= 0x7e; |
| } |
| |
| static bool DigestsEqual(const DigestValue& digest1, |
| const DigestValue& digest2) { |
| if (digest1.size() != digest2.size()) |
| return false; |
| |
| for (wtf_size_t i = 0; i < digest1.size(); i++) { |
| if (digest1[i] != digest2[i]) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void SubresourceIntegrity::ReportInfo::AddUseCount(UseCounterFeature feature) { |
| use_counts_.push_back(feature); |
| } |
| |
| void SubresourceIntegrity::ReportInfo::AddConsoleErrorMessage( |
| const String& message) { |
| console_error_messages_.push_back(message); |
| } |
| |
| void SubresourceIntegrity::ReportInfo::Clear() { |
| use_counts_.clear(); |
| console_error_messages_.clear(); |
| } |
| |
| bool SubresourceIntegrity::CheckSubresourceIntegrity( |
| const IntegrityMetadataSet& metadata_set, |
| const char* content, |
| size_t size, |
| const KURL& resource_url, |
| const Resource& resource, |
| ReportInfo& report_info) { |
| // FetchResponseType::kError never arrives because it is a loading error. |
| DCHECK_NE(resource.GetResponse().GetType(), |
| network::mojom::FetchResponseType::kError); |
| if (!resource.GetResponse().IsCorsSameOrigin()) { |
| report_info.AddConsoleErrorMessage( |
| "Subresource Integrity: The resource '" + resource_url.ElidedString() + |
| "' has an integrity attribute, but the resource " |
| "requires the request to be CORS enabled to check " |
| "the integrity, and it is not. The resource has been " |
| "blocked because the integrity cannot be enforced."); |
| report_info.AddUseCount(ReportInfo::UseCounterFeature:: |
| kSRIElementIntegrityAttributeButIneligible); |
| return false; |
| } |
| |
| return CheckSubresourceIntegrityImpl(metadata_set, content, size, |
| resource_url, report_info); |
| } |
| |
| bool SubresourceIntegrity::CheckSubresourceIntegrity( |
| const String& integrity_metadata, |
| IntegrityFeatures features, |
| const char* content, |
| size_t size, |
| const KURL& resource_url, |
| ReportInfo& report_info) { |
| if (integrity_metadata.IsEmpty()) |
| return true; |
| |
| IntegrityMetadataSet metadata_set; |
| IntegrityParseResult integrity_parse_result = ParseIntegrityAttribute( |
| integrity_metadata, features, metadata_set, &report_info); |
| if (integrity_parse_result != kIntegrityParseValidResult) |
| return true; |
| return CheckSubresourceIntegrityImpl(metadata_set, content, size, |
| resource_url, report_info); |
| } |
| |
| bool SubresourceIntegrity::CheckSubresourceIntegrityImpl( |
| const IntegrityMetadataSet& metadata_set, |
| const char* content, |
| size_t size, |
| const KURL& resource_url, |
| ReportInfo& report_info) { |
| if (!metadata_set.size()) |
| return true; |
| |
| // Check any of the "strongest" integrity constraints. |
| IntegrityAlgorithm max_algorithm = FindBestAlgorithm(metadata_set); |
| for (const IntegrityMetadata& metadata : metadata_set) { |
| if (metadata.Algorithm() == max_algorithm && |
| CheckSubresourceIntegrityDigest(metadata, content, size)) { |
| report_info.AddUseCount(ReportInfo::UseCounterFeature:: |
| kSRIElementWithMatchingIntegrityAttribute); |
| return true; |
| } |
| } |
| |
| // If we arrive here, none of the "strongest" constaints have validated |
| // the data we received. Report this fact. |
| DigestValue digest; |
| if (ComputeDigest(kHashAlgorithmSha256, content, size, digest)) { |
| // This message exposes the digest of the resource to the console. |
| // Because this is only to the console, that's okay for now, but we |
| // need to be very careful not to expose this in exceptions or |
| // JavaScript, otherwise it risks exposing information about the |
| // resource cross-origin. |
| report_info.AddConsoleErrorMessage( |
| "Failed to find a valid digest in the 'integrity' attribute for " |
| "resource '" + |
| resource_url.ElidedString() + "' with computed SHA-256 integrity '" + |
| Base64Encode(digest) + "'. The resource has been blocked."); |
| } else { |
| report_info.AddConsoleErrorMessage( |
| "There was an error computing an integrity value for resource '" + |
| resource_url.ElidedString() + "'. The resource has been blocked."); |
| } |
| report_info.AddUseCount(ReportInfo::UseCounterFeature:: |
| kSRIElementWithNonMatchingIntegrityAttribute); |
| return false; |
| } |
| |
| IntegrityAlgorithm SubresourceIntegrity::FindBestAlgorithm( |
| const IntegrityMetadataSet& metadata_set) { |
| // Find the "strongest" algorithm in the set. (This relies on |
| // IntegrityAlgorithm declaration order matching the "strongest" order, so |
| // make the compiler check this assumption first.) |
| static_assert(IntegrityAlgorithm::kSha256 < IntegrityAlgorithm::kSha384 && |
| IntegrityAlgorithm::kSha384 < IntegrityAlgorithm::kSha512, |
| "IntegrityAlgorithm enum order should match the priority " |
| "of the integrity algorithms."); |
| |
| // metadata_set is non-empty, so we are guaranteed to always have a result. |
| // This is effectively an implemenation of std::max_element (C++17). |
| DCHECK(!metadata_set.IsEmpty()); |
| auto iter = metadata_set.begin(); |
| IntegrityAlgorithm max_algorithm = iter->second; |
| ++iter; |
| for (; iter != metadata_set.end(); ++iter) { |
| max_algorithm = std::max(iter->second, max_algorithm); |
| } |
| return max_algorithm; |
| } |
| |
| bool SubresourceIntegrity::CheckSubresourceIntegrityDigest( |
| const IntegrityMetadata& metadata, |
| const char* content, |
| size_t size) { |
| blink::HashAlgorithm hash_algo = kHashAlgorithmSha256; |
| switch (metadata.Algorithm()) { |
| case IntegrityAlgorithm::kSha256: |
| hash_algo = kHashAlgorithmSha256; |
| break; |
| case IntegrityAlgorithm::kSha384: |
| hash_algo = kHashAlgorithmSha384; |
| break; |
| case IntegrityAlgorithm::kSha512: |
| hash_algo = kHashAlgorithmSha512; |
| break; |
| } |
| |
| DigestValue digest; |
| if (!ComputeDigest(hash_algo, content, size, digest)) |
| return false; |
| |
| Vector<char> hash_vector; |
| Base64Decode(metadata.Digest(), hash_vector); |
| DigestValue converted_hash_vector; |
| converted_hash_vector.Append(reinterpret_cast<uint8_t*>(hash_vector.data()), |
| hash_vector.size()); |
| return DigestsEqual(digest, converted_hash_vector); |
| } |
| |
| SubresourceIntegrity::AlgorithmParseResult |
| SubresourceIntegrity::ParseAttributeAlgorithm(const UChar*& begin, |
| const UChar* end, |
| IntegrityFeatures features, |
| IntegrityAlgorithm& algorithm) { |
| static const AlgorithmPrefixPair kPrefixes[] = { |
| {"sha256", IntegrityAlgorithm::kSha256}, |
| {"sha-256", IntegrityAlgorithm::kSha256}, |
| {"sha384", IntegrityAlgorithm::kSha384}, |
| {"sha-384", IntegrityAlgorithm::kSha384}, |
| {"sha512", IntegrityAlgorithm::kSha512}, |
| {"sha-512", IntegrityAlgorithm::kSha512}}; |
| |
| // The last algorithm prefix is the ed25519 signature algorithm, which should |
| // only be enabled if kSignatures is requested. We'll implement this by |
| // adjusting the last_prefix index into the array. |
| size_t last_prefix = base::size(kPrefixes); |
| if (features != IntegrityFeatures::kSignatures) |
| last_prefix--; |
| |
| return ParseAlgorithmPrefix(begin, end, kPrefixes, last_prefix, algorithm); |
| } |
| |
| SubresourceIntegrity::AlgorithmParseResult |
| SubresourceIntegrity::ParseAlgorithmPrefix( |
| const UChar*& string_position, |
| const UChar* string_end, |
| const AlgorithmPrefixPair* prefix_table, |
| size_t prefix_table_size, |
| IntegrityAlgorithm& algorithm) { |
| for (size_t i = 0; i < prefix_table_size; i++) { |
| const UChar* pos = string_position; |
| if (SkipToken<UChar>(pos, string_end, prefix_table[i].first) && |
| SkipExactly<UChar>(pos, string_end, '-')) { |
| string_position = pos; |
| algorithm = prefix_table[i].second; |
| return kAlgorithmValid; |
| } |
| } |
| |
| const UChar* dash_position = string_position; |
| SkipUntil<UChar>(dash_position, string_end, '-'); |
| return dash_position < string_end ? kAlgorithmUnknown : kAlgorithmUnparsable; |
| } |
| |
| // Before: |
| // |
| // [algorithm]-[hash] OR [algorithm]-[hash]?[options] |
| // ^ ^ ^ ^ |
| // position end position end |
| // |
| // After (if successful: if the method returns false, we make no promises and |
| // the caller should exit early): |
| // |
| // [algorithm]-[hash] OR [algorithm]-[hash]?[options] |
| // ^ ^ ^ |
| // position/end position end |
| bool SubresourceIntegrity::ParseDigest(const UChar*& position, |
| const UChar* end, |
| String& digest) { |
| const UChar* begin = position; |
| SkipWhile<UChar, IsIntegrityCharacter>(position, end); |
| if (position == begin || (position != end && *position != '?')) { |
| digest = g_empty_string; |
| return false; |
| } |
| |
| // We accept base64url encoding, but normalize to "normal" base64 internally: |
| digest = NormalizeToBase64( |
| String(begin, static_cast<wtf_size_t>(position - begin))); |
| return true; |
| } |
| |
| SubresourceIntegrity::IntegrityParseResult |
| SubresourceIntegrity::ParseIntegrityAttribute( |
| const WTF::String& attribute, |
| IntegrityFeatures features, |
| IntegrityMetadataSet& metadata_set) { |
| return ParseIntegrityAttribute(attribute, features, metadata_set, nullptr); |
| } |
| |
| SubresourceIntegrity::IntegrityParseResult |
| SubresourceIntegrity::ParseIntegrityAttribute( |
| const WTF::String& attribute, |
| IntegrityFeatures features, |
| IntegrityMetadataSet& metadata_set, |
| ReportInfo* report_info) { |
| // We expect a "clean" metadata_set, since metadata_set should only be filled |
| // once. |
| DCHECK(metadata_set.IsEmpty()); |
| |
| Vector<UChar> characters; |
| attribute.StripWhiteSpace().AppendTo(characters); |
| const UChar* position = characters.data(); |
| const UChar* end = characters.end(); |
| const UChar* current_integrity_end; |
| |
| bool error = false; |
| |
| // The integrity attribute takes the form: |
| // *WSP hash-with-options *( 1*WSP hash-with-options ) *WSP / *WSP |
| // To parse this, break on whitespace, parsing each algorithm/digest/option |
| // in order. |
| while (position < end) { |
| WTF::String digest; |
| IntegrityAlgorithm algorithm; |
| |
| SkipWhile<UChar, IsASCIISpace>(position, end); |
| current_integrity_end = position; |
| SkipUntil<UChar, IsASCIISpace>(current_integrity_end, end); |
| |
| // Algorithm parsing errors are non-fatal (the subresource should |
| // still be loaded) because strong hash algorithms should be used |
| // without fear of breaking older user agents that don't support |
| // them. |
| AlgorithmParseResult parse_result = ParseAttributeAlgorithm( |
| position, current_integrity_end, features, algorithm); |
| if (parse_result == kAlgorithmUnknown) { |
| // Unknown hash algorithms are treated as if they're not present, |
| // and thus are not marked as an error, they're just skipped. |
| SkipUntil<UChar, IsASCIISpace>(position, end); |
| if (report_info) { |
| report_info->AddConsoleErrorMessage( |
| "Error parsing 'integrity' attribute ('" + attribute + |
| "'). The specified hash algorithm must be one of " |
| "'sha256', 'sha384', or 'sha512'."); |
| report_info->AddUseCount( |
| ReportInfo::UseCounterFeature:: |
| kSRIElementWithUnparsableIntegrityAttribute); |
| } |
| continue; |
| } |
| |
| if (parse_result == kAlgorithmUnparsable) { |
| error = true; |
| SkipUntil<UChar, IsASCIISpace>(position, end); |
| if (report_info) { |
| report_info->AddConsoleErrorMessage( |
| "Error parsing 'integrity' attribute ('" + attribute + |
| "'). The hash algorithm must be one of 'sha256', " |
| "'sha384', or 'sha512', followed by a '-' " |
| "character."); |
| report_info->AddUseCount( |
| ReportInfo::UseCounterFeature:: |
| kSRIElementWithUnparsableIntegrityAttribute); |
| } |
| continue; |
| } |
| |
| DCHECK_EQ(parse_result, kAlgorithmValid); |
| |
| if (!ParseDigest(position, current_integrity_end, digest)) { |
| error = true; |
| SkipUntil<UChar, IsASCIISpace>(position, end); |
| if (report_info) { |
| report_info->AddConsoleErrorMessage( |
| "Error parsing 'integrity' attribute ('" + attribute + |
| "'). The digest must be a valid, base64-encoded value."); |
| report_info->AddUseCount( |
| ReportInfo::UseCounterFeature:: |
| kSRIElementWithUnparsableIntegrityAttribute); |
| } |
| continue; |
| } |
| |
| // The spec defines a space in the syntax for options, separated by a |
| // '?' character followed by unbounded VCHARs, but no actual options |
| // have been defined yet. Thus, for forward compatibility, ignore any |
| // options specified. |
| if (SkipExactly<UChar>(position, end, '?')) { |
| const UChar* begin = position; |
| SkipWhile<UChar, IsValueCharacter>(position, end); |
| if (begin != position && report_info) { |
| report_info->AddConsoleErrorMessage( |
| "Ignoring unrecogized 'integrity' attribute option '" + |
| String(begin, static_cast<wtf_size_t>(position - begin)) + "'."); |
| } |
| } |
| |
| IntegrityMetadata integrity_metadata(digest, algorithm); |
| metadata_set.insert(integrity_metadata.ToPair()); |
| } |
| if (metadata_set.size() == 0 && error) |
| return kIntegrityParseNoValidResult; |
| |
| return kIntegrityParseValidResult; |
| } |
| |
| } // namespace blink |