blob: f705c0bf9b872494b9cad18cc78ea906ab12d6a2 [file] [log] [blame]
// 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 "platform/loader/SubresourceIntegrity.h"
#include "platform/Crypto.h"
#include "platform/loader/fetch/Resource.h"
#include "platform/weborigin/KURL.h"
#include "platform/weborigin/SecurityOrigin.h"
#include "platform/wtf/ASCIICType.h"
#include "platform/wtf/Vector.h"
#include "platform/wtf/dtoa/utils.h"
#include "platform/wtf/text/Base64.h"
#include "platform/wtf/text/ParsingUtilities.h"
#include "platform/wtf/text/StringUTF8Adaptor.h"
#include "platform/wtf/text/WTFString.h"
#include "public/platform/WebCrypto.h"
#include "public/platform/WebCryptoAlgorithm.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 (size_t i = 0; i < digest1.size(); i++) {
if (digest1[i] != digest2[i])
return false;
}
return true;
}
static String DigestToString(const DigestValue& digest) {
return Base64Encode(reinterpret_cast<const char*>(digest.data()),
digest.size(), kBase64DoNotInsertLFs);
}
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();
}
IntegrityAlgorithm SubresourceIntegrity::GetPrioritizedHashFunction(
IntegrityAlgorithm algorithm1,
IntegrityAlgorithm algorithm2) {
const IntegrityAlgorithm kWeakerThanSha384[] = {IntegrityAlgorithm::kSha256};
const IntegrityAlgorithm kWeakerThanSha512[] = {IntegrityAlgorithm::kSha256,
IntegrityAlgorithm::kSha384};
if (algorithm1 == algorithm2)
return algorithm1;
const IntegrityAlgorithm* weaker_algorithms = 0;
size_t length = 0;
switch (algorithm1) {
case IntegrityAlgorithm::kSha256:
break;
case IntegrityAlgorithm::kSha384:
weaker_algorithms = kWeakerThanSha384;
length = WTF_ARRAY_LENGTH(kWeakerThanSha384);
break;
case IntegrityAlgorithm::kSha512:
weaker_algorithms = kWeakerThanSha512;
length = WTF_ARRAY_LENGTH(kWeakerThanSha512);
break;
default:
NOTREACHED();
};
for (size_t i = 0; i < length; i++) {
if (weaker_algorithms[i] == algorithm2)
return algorithm1;
}
return algorithm2;
}
bool SubresourceIntegrity::CheckSubresourceIntegrity(
const String& integrity_attribute,
const char* content,
size_t size,
const KURL& resource_url,
const Resource& resource,
ReportInfo& report_info) {
if (integrity_attribute.IsEmpty())
return true;
IntegrityMetadataSet metadata_set;
IntegrityParseResult integrity_parse_result =
ParseIntegrityAttribute(integrity_attribute, metadata_set, &report_info);
if (integrity_parse_result != kIntegrityParseValidResult)
return true;
return CheckSubresourceIntegrity(metadata_set, content, size, resource_url,
resource, report_info);
}
bool SubresourceIntegrity::CheckSubresourceIntegrity(
const IntegrityMetadataSet& metadata_set,
const char* content,
size_t size,
const KURL& resource_url,
const Resource& resource,
ReportInfo& report_info) {
if (!resource.IsSameOriginOrCORSSuccessful()) {
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 CheckSubresourceIntegrity(metadata_set, content, size, resource_url,
report_info);
}
bool SubresourceIntegrity::CheckSubresourceIntegrity(
const String& integrity_metadata,
const char* content,
size_t size,
const KURL& resource_url,
ReportInfo& report_info) {
IntegrityMetadataSet metadata_set;
IntegrityParseResult integrity_parse_result =
ParseIntegrityAttribute(integrity_metadata, metadata_set, &report_info);
if (integrity_parse_result != kIntegrityParseValidResult)
return true;
return CheckSubresourceIntegrity(metadata_set, content, size, resource_url,
report_info);
}
bool SubresourceIntegrity::CheckSubresourceIntegrity(
const IntegrityMetadataSet& metadata_set,
const char* content,
size_t size,
const KURL& resource_url,
ReportInfo& report_info) {
if (!metadata_set.size())
return true;
IntegrityAlgorithm strongest_algorithm = IntegrityAlgorithm::kSha256;
for (const IntegrityMetadata& metadata : metadata_set) {
strongest_algorithm =
GetPrioritizedHashFunction(metadata.Algorithm(), strongest_algorithm);
}
DigestValue digest;
for (const IntegrityMetadata& metadata : metadata_set) {
if (metadata.Algorithm() != strongest_algorithm)
continue;
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;
}
digest.clear();
bool digest_success = ComputeDigest(hash_algo, content, size, digest);
if (digest_success) {
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());
if (DigestsEqual(digest, converted_hash_vector)) {
report_info.AddUseCount(ReportInfo::UseCounterFeature::
kSRIElementWithMatchingIntegrityAttribute);
return true;
}
}
}
digest.clear();
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 '" +
DigestToString(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;
}
// Before:
//
// [algorithm]-[hash]
// ^ ^
// position end
//
// After (if successful: if the method does not return AlgorithmValid, we make
// no promises and the caller should exit early):
//
// [algorithm]-[hash]
// ^ ^
// position end
SubresourceIntegrity::AlgorithmParseResult SubresourceIntegrity::ParseAlgorithm(
const UChar*& position,
const UChar* end,
IntegrityAlgorithm& algorithm) {
// Any additions or subtractions from this struct should also modify the
// respective entries in the kAlgorithmMap array in checkDigest() as well
// as the array in algorithmToString().
static const struct {
const char* prefix;
IntegrityAlgorithm algorithm;
} kSupportedPrefixes[] = {{"sha256", IntegrityAlgorithm::kSha256},
{"sha-256", IntegrityAlgorithm::kSha256},
{"sha384", IntegrityAlgorithm::kSha384},
{"sha-384", IntegrityAlgorithm::kSha384},
{"sha512", IntegrityAlgorithm::kSha512},
{"sha-512", IntegrityAlgorithm::kSha512}};
const UChar* begin = position;
for (auto& prefix : kSupportedPrefixes) {
if (SkipToken<UChar>(position, end, prefix.prefix)) {
if (!SkipExactly<UChar>(position, end, '-')) {
position = begin;
continue;
}
algorithm = prefix.algorithm;
return kAlgorithmValid;
}
}
SkipUntil<UChar>(position, end, '-');
if (position < end && *position == '-') {
position = begin;
return kAlgorithmUnknown;
}
position = begin;
return 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, position - begin));
return true;
}
SubresourceIntegrity::IntegrityParseResult
SubresourceIntegrity::ParseIntegrityAttribute(
const WTF::String& attribute,
IntegrityMetadataSet& metadata_set) {
return ParseIntegrityAttribute(attribute, metadata_set, nullptr);
}
SubresourceIntegrity::IntegrityParseResult
SubresourceIntegrity::ParseIntegrityAttribute(
const WTF::String& attribute,
IntegrityMetadataSet& metadata_set,
ReportInfo* report_info) {
Vector<UChar> characters;
attribute.StripWhiteSpace().AppendTo(characters);
const UChar* position = characters.data();
const UChar* end = characters.end();
const UChar* current_integrity_end;
metadata_set.clear();
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 =
ParseAlgorithm(position, current_integrity_end, 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, 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