1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 #include "core/frame/SubresourceIntegrity.h"
8 #include "core/HTMLNames.h"
9 #include "core/dom/Document.h"
10 #include "core/dom/Element.h"
11 #include "core/fetch/Resource.h"
12 #include "core/frame/ConsoleTypes.h"
13 #include "core/frame/UseCounter.h"
14 #include "core/inspector/ConsoleMessage.h"
15 #include "platform/Crypto.h"
16 #include "platform/ParsingUtilities.h"
17 #include "platform/weborigin/KURL.h"
18 #include "platform/weborigin/SecurityOrigin.h"
19 #include "public/platform/WebCrypto.h"
20 #include "public/platform/WebCryptoAlgorithm.h"
21 #include "wtf/ASCIICType.h"
22 #include "wtf/Vector.h"
23 #include "wtf/dtoa/utils.h"
24 #include "wtf/text/Base64.h"
25 #include "wtf/text/StringUTF8Adaptor.h"
26 #include "wtf/text/WTFString.h"
30 // FIXME: This should probably use common functions with ContentSecurityPolicy.
31 static bool isIntegrityCharacter(UChar c
)
33 // Check if it's a base64 encoded value. We're pretty loose here, as there's
34 // not much risk in it, and it'll make it simpler for developers.
35 return isASCIIAlphanumeric(c
) || c
== '_' || c
== '-' || c
== '+' || c
== '/' || c
== '=';
38 static bool isValueCharacter(UChar c
)
40 // VCHAR per https://tools.ietf.org/html/rfc5234#appendix-B.1
41 return c
>= 0x21 && c
<= 0x7e;
44 static void logErrorToConsole(const String
& message
, Document
& document
)
46 document
.addConsoleMessage(ConsoleMessage::create(SecurityMessageSource
, ErrorMessageLevel
, message
));
49 static bool DigestsEqual(const DigestValue
& digest1
, const DigestValue
& digest2
)
51 if (digest1
.size() != digest2
.size())
54 for (size_t i
= 0; i
< digest1
.size(); i
++) {
55 if (digest1
[i
] != digest2
[i
])
62 static String
digestToString(const DigestValue
& digest
)
64 return base64Encode(reinterpret_cast<const char*>(digest
.data()), digest
.size(), Base64DoNotInsertLFs
);
68 HashAlgorithm
SubresourceIntegrity::getPrioritizedHashFunction(HashAlgorithm algorithm1
, HashAlgorithm algorithm2
)
70 const HashAlgorithm weakerThanSha384
[] = { HashAlgorithmSha256
};
71 const HashAlgorithm weakerThanSha512
[] = { HashAlgorithmSha256
, HashAlgorithmSha384
};
73 ASSERT(algorithm1
!= HashAlgorithmSha1
);
74 ASSERT(algorithm2
!= HashAlgorithmSha1
);
76 if (algorithm1
== algorithm2
)
79 const HashAlgorithm
* weakerAlgorithms
= 0;
82 case HashAlgorithmSha256
:
84 case HashAlgorithmSha384
:
85 weakerAlgorithms
= weakerThanSha384
;
86 length
= ARRAY_SIZE(weakerThanSha384
);
88 case HashAlgorithmSha512
:
89 weakerAlgorithms
= weakerThanSha512
;
90 length
= ARRAY_SIZE(weakerThanSha512
);
96 for (size_t i
= 0; i
< length
; i
++) {
97 if (weakerAlgorithms
[i
] == algorithm2
)
104 bool SubresourceIntegrity::CheckSubresourceIntegrity(const Element
& element
, const String
& source
, const KURL
& resourceUrl
, const Resource
& resource
)
106 Document
& document
= element
.document();
107 String attribute
= element
.fastGetAttribute(HTMLNames::integrityAttr
);
108 if (attribute
.isEmpty())
111 if (!resource
.isEligibleForIntegrityCheck(document
.securityOrigin())) {
112 UseCounter::count(document
, UseCounter::SRIElementIntegrityAttributeButIneligible
);
113 logErrorToConsole("Subresource Integrity: The resource '" + resourceUrl
.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.", document
);
118 bool result
= CheckSubresourceIntegrity(attribute
, source
, resourceUrl
, document
, errorMessage
);
120 logErrorToConsole(errorMessage
, document
);
124 bool SubresourceIntegrity::CheckSubresourceIntegrity(const String
& integrityMetadata
, const WTF::String
& source
, const KURL
& resourceUrl
, Document
& document
, String
& errorMessage
)
126 WTF::Vector
<IntegrityMetadata
> metadataList
;
127 IntegrityParseResult integrityParseResult
= parseIntegrityAttribute(integrityMetadata
, metadataList
, document
);
128 // On failed parsing, there's no need to log an error here, as
129 // parseIntegrityAttribute() will output an appropriate console message.
130 if (integrityParseResult
!= IntegrityParseValidResult
)
133 StringUTF8Adaptor
normalizedSource(source
, StringUTF8Adaptor::Normalize
, WTF::EntitiesForUnencodables
);
135 if (!metadataList
.size())
138 HashAlgorithm strongestAlgorithm
= HashAlgorithmSha256
;
139 for (const IntegrityMetadata
& metadata
: metadataList
)
140 strongestAlgorithm
= getPrioritizedHashFunction(metadata
.algorithm
, strongestAlgorithm
);
143 for (const IntegrityMetadata
& metadata
: metadataList
) {
144 if (metadata
.algorithm
!= strongestAlgorithm
)
148 bool digestSuccess
= computeDigest(metadata
.algorithm
, normalizedSource
.data(), normalizedSource
.length(), digest
);
151 Vector
<char> hashVector
;
152 base64Decode(metadata
.digest
, hashVector
);
153 DigestValue convertedHashVector
;
154 convertedHashVector
.append(reinterpret_cast<uint8_t*>(hashVector
.data()), hashVector
.size());
156 if (DigestsEqual(digest
, convertedHashVector
)) {
157 UseCounter::count(document
, UseCounter::SRIElementWithMatchingIntegrityAttribute
);
164 if (computeDigest(HashAlgorithmSha256
, normalizedSource
.data(), normalizedSource
.length(), digest
)) {
165 // This message exposes the digest of the resource to the console.
166 // Because this is only to the console, that's okay for now, but we
167 // need to be very careful not to expose this in exceptions or
168 // JavaScript, otherwise it risks exposing information about the
169 // resource cross-origin.
170 errorMessage
= "Failed to find a valid digest in the 'integrity' attribute for resource '" + resourceUrl
.elidedString() + "' with computed SHA-256 integrity '" + digestToString(digest
) + "'. The resource has been blocked.";
172 errorMessage
= "There was an error computing an integrity value for resource '" + resourceUrl
.elidedString() + "'. The resource has been blocked.";
174 UseCounter::count(document
, UseCounter::SRIElementWithNonMatchingIntegrityAttribute
);
180 // [algorithm]-[hash]
184 // After (if successful: if the method does not return AlgorithmValid, we make
185 // no promises and the caller should exit early):
187 // [algorithm]-[hash]
190 SubresourceIntegrity::AlgorithmParseResult
SubresourceIntegrity::parseAlgorithm(const UChar
*& position
, const UChar
* end
, HashAlgorithm
& algorithm
)
192 // Any additions or subtractions from this struct should also modify the
193 // respective entries in the kAlgorithmMap array in checkDigest() as well
194 // as the array in algorithmToString().
195 static const struct {
197 HashAlgorithm algorithm
;
198 } kSupportedPrefixes
[] = {
199 { "sha256", HashAlgorithmSha256
},
200 { "sha-256", HashAlgorithmSha256
},
201 { "sha384", HashAlgorithmSha384
},
202 { "sha-384", HashAlgorithmSha384
},
203 { "sha512", HashAlgorithmSha512
},
204 { "sha-512", HashAlgorithmSha512
}
207 const UChar
* begin
= position
;
209 for (auto& prefix
: kSupportedPrefixes
) {
210 if (skipToken
<UChar
>(position
, end
, prefix
.prefix
)) {
211 if (!skipExactly
<UChar
>(position
, end
, '-')) {
215 algorithm
= prefix
.algorithm
;
216 return AlgorithmValid
;
220 skipUntil
<UChar
>(position
, end
, '-');
221 if (position
< end
&& *position
== '-') {
223 return AlgorithmUnknown
;
227 return AlgorithmUnparsable
;
232 // [algorithm]-[hash] OR [algorithm]-[hash]?[options]
234 // position end position end
236 // After (if successful: if the method returns false, we make no promises and the caller should exit early):
238 // [algorithm]-[hash] OR [algorithm]-[hash]?[options]
240 // position/end position end
241 bool SubresourceIntegrity::parseDigest(const UChar
*& position
, const UChar
* end
, String
& digest
)
243 const UChar
* begin
= position
;
244 skipWhile
<UChar
, isIntegrityCharacter
>(position
, end
);
246 if (position
== begin
|| (position
!= end
&& *position
!= '?')) {
247 digest
= emptyString();
251 // We accept base64url encoding, but normalize to "normal" base64 internally:
252 digest
= normalizeToBase64(String(begin
, position
- begin
));
256 SubresourceIntegrity::IntegrityParseResult
SubresourceIntegrity::parseIntegrityAttribute(const WTF::String
& attribute
, WTF::Vector
<IntegrityMetadata
>& metadataList
, Document
& document
)
258 Vector
<UChar
> characters
;
259 attribute
.stripWhiteSpace().appendTo(characters
);
260 const UChar
* position
= characters
.data();
261 const UChar
* end
= characters
.end();
262 const UChar
* currentIntegrityEnd
;
264 metadataList
.clear();
267 // The integrity attribute takes the form:
268 // *WSP hash-with-options *( 1*WSP hash-with-options ) *WSP / *WSP
269 // To parse this, break on whitespace, parsing each algorithm/digest/option
271 while (position
< end
) {
273 HashAlgorithm algorithm
;
275 skipWhile
<UChar
, isASCIISpace
>(position
, end
);
276 currentIntegrityEnd
= position
;
277 skipUntil
<UChar
, isASCIISpace
>(currentIntegrityEnd
, end
);
279 // Algorithm parsing errors are non-fatal (the subresource should
280 // still be loaded) because strong hash algorithms should be used
281 // without fear of breaking older user agents that don't support
283 AlgorithmParseResult parseResult
= parseAlgorithm(position
, currentIntegrityEnd
, algorithm
);
284 if (parseResult
== AlgorithmUnknown
) {
285 // Unknown hash algorithms are treated as if they're not present,
286 // and thus are not marked as an error, they're just skipped.
287 logErrorToConsole("Error parsing 'integrity' attribute ('" + attribute
+ "'). The specified hash algorithm must be one of 'sha256', 'sha384', or 'sha512'.", document
);
288 skipUntil
<UChar
, isASCIISpace
>(position
, end
);
289 UseCounter::count(document
, UseCounter::SRIElementWithUnparsableIntegrityAttribute
);
293 if (parseResult
== AlgorithmUnparsable
) {
294 logErrorToConsole("Error parsing 'integrity' attribute ('" + attribute
+ "'). The hash algorithm must be one of 'sha256', 'sha384', or 'sha512', followed by a '-' character.", document
);
296 UseCounter::count(document
, UseCounter::SRIElementWithUnparsableIntegrityAttribute
);
297 skipUntil
<UChar
, isASCIISpace
>(position
, end
);
301 ASSERT(parseResult
== AlgorithmValid
);
303 if (!parseDigest(position
, currentIntegrityEnd
, digest
)) {
304 logErrorToConsole("Error parsing 'integrity' attribute ('" + attribute
+ "'). The digest must be a valid, base64-encoded value.", document
);
306 skipUntil
<UChar
, isASCIISpace
>(position
, end
);
307 UseCounter::count(document
, UseCounter::SRIElementWithUnparsableIntegrityAttribute
);
311 // The spec defines a space in the syntax for options, separated by a
312 // '?' character followed by unbounded VCHARs, but no actual options
313 // have been defined yet. Thus, for forward compatibility, ignore any
314 // options specified.
315 if (skipExactly
<UChar
>(position
, end
, '?')) {
316 const UChar
* begin
= position
;
317 skipWhile
<UChar
, isValueCharacter
>(position
, end
);
318 if (begin
!= position
)
319 logErrorToConsole("Ignoring unrecogized 'integrity' attribute option '" + String(begin
, position
- begin
) + "'.", document
);
322 IntegrityMetadata integrityMetadata
= {
326 metadataList
.append(integrityMetadata
);
329 if (metadataList
.size() == 0 && error
)
330 return IntegrityParseNoValidResult
;
332 return IntegrityParseValidResult
;