OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "core/frame/SubresourceIntegrity.h" | 5 #include "core/frame/SubresourceIntegrity.h" |
6 | 6 |
7 #include "core/HTMLNames.h" | 7 #include "core/HTMLNames.h" |
8 #include "core/dom/Document.h" | 8 #include "core/dom/Document.h" |
9 #include "core/dom/Element.h" | 9 #include "core/dom/Element.h" |
| 10 #include "core/dom/ExecutionContext.h" |
10 #include "core/fetch/Resource.h" | 11 #include "core/fetch/Resource.h" |
11 #include "core/frame/UseCounter.h" | 12 #include "core/frame/UseCounter.h" |
12 #include "core/inspector/ConsoleMessage.h" | 13 #include "core/inspector/ConsoleMessage.h" |
13 #include "platform/Crypto.h" | 14 #include "platform/Crypto.h" |
14 #include "platform/weborigin/KURL.h" | 15 #include "platform/weborigin/KURL.h" |
15 #include "platform/weborigin/SecurityOrigin.h" | 16 #include "platform/weborigin/SecurityOrigin.h" |
16 #include "public/platform/WebCrypto.h" | 17 #include "public/platform/WebCrypto.h" |
17 #include "public/platform/WebCryptoAlgorithm.h" | 18 #include "public/platform/WebCryptoAlgorithm.h" |
18 #include "wtf/ASCIICType.h" | 19 #include "wtf/ASCIICType.h" |
19 #include "wtf/Vector.h" | 20 #include "wtf/Vector.h" |
(...skipping 11 matching lines...) Expand all Loading... |
31 // not much risk in it, and it'll make it simpler for developers. | 32 // not much risk in it, and it'll make it simpler for developers. |
32 return isASCIIAlphanumeric(c) || c == '_' || c == '-' || c == '+' || | 33 return isASCIIAlphanumeric(c) || c == '_' || c == '-' || c == '+' || |
33 c == '/' || c == '='; | 34 c == '/' || c == '='; |
34 } | 35 } |
35 | 36 |
36 static bool isValueCharacter(UChar c) { | 37 static bool isValueCharacter(UChar c) { |
37 // VCHAR per https://tools.ietf.org/html/rfc5234#appendix-B.1 | 38 // VCHAR per https://tools.ietf.org/html/rfc5234#appendix-B.1 |
38 return c >= 0x21 && c <= 0x7e; | 39 return c >= 0x21 && c <= 0x7e; |
39 } | 40 } |
40 | 41 |
41 static void logErrorToConsole(const String& message, Document& document) { | 42 static void logErrorToConsole(const String& message, |
42 document.addConsoleMessage(ConsoleMessage::create( | 43 ExecutionContext& executionContext) { |
| 44 executionContext.addConsoleMessage(ConsoleMessage::create( |
43 SecurityMessageSource, ErrorMessageLevel, message)); | 45 SecurityMessageSource, ErrorMessageLevel, message)); |
44 } | 46 } |
45 | 47 |
46 static bool DigestsEqual(const DigestValue& digest1, | 48 static bool DigestsEqual(const DigestValue& digest1, |
47 const DigestValue& digest2) { | 49 const DigestValue& digest2) { |
48 if (digest1.size() != digest2.size()) | 50 if (digest1.size() != digest2.size()) |
49 return false; | 51 return false; |
50 | 52 |
51 for (size_t i = 0; i < digest1.size(); i++) { | 53 for (size_t i = 0; i < digest1.size(); i++) { |
52 if (digest1[i] != digest2[i]) | 54 if (digest1[i] != digest2[i]) |
(...skipping 96 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
149 if (!result) | 151 if (!result) |
150 logErrorToConsole(errorMessage, document); | 152 logErrorToConsole(errorMessage, document); |
151 return result; | 153 return result; |
152 } | 154 } |
153 | 155 |
154 bool SubresourceIntegrity::CheckSubresourceIntegrity( | 156 bool SubresourceIntegrity::CheckSubresourceIntegrity( |
155 const String& integrityMetadata, | 157 const String& integrityMetadata, |
156 const char* content, | 158 const char* content, |
157 size_t size, | 159 size_t size, |
158 const KURL& resourceUrl, | 160 const KURL& resourceUrl, |
159 Document& document, | 161 ExecutionContext& executionContext, |
160 String& errorMessage) { | 162 String& errorMessage) { |
161 IntegrityMetadataSet metadataSet; | 163 IntegrityMetadataSet metadataSet; |
162 IntegrityParseResult integrityParseResult = | 164 IntegrityParseResult integrityParseResult = parseIntegrityAttribute( |
163 parseIntegrityAttribute(integrityMetadata, metadataSet, &document); | 165 integrityMetadata, metadataSet, &executionContext); |
164 // On failed parsing, there's no need to log an error here, as | 166 // On failed parsing, there's no need to log an error here, as |
165 // parseIntegrityAttribute() will output an appropriate console message. | 167 // parseIntegrityAttribute() will output an appropriate console message. |
166 if (integrityParseResult != IntegrityParseValidResult) | 168 if (integrityParseResult != IntegrityParseValidResult) |
167 return true; | 169 return true; |
168 | 170 |
169 return CheckSubresourceIntegrity(metadataSet, content, size, resourceUrl, | 171 return CheckSubresourceIntegrity(metadataSet, content, size, resourceUrl, |
170 document, errorMessage); | 172 executionContext, errorMessage); |
171 } | 173 } |
172 | 174 |
173 bool SubresourceIntegrity::CheckSubresourceIntegrity( | 175 bool SubresourceIntegrity::CheckSubresourceIntegrity( |
174 const IntegrityMetadataSet& metadataSet, | 176 const IntegrityMetadataSet& metadataSet, |
175 const char* content, | 177 const char* content, |
176 size_t size, | 178 size_t size, |
177 const KURL& resourceUrl, | 179 const KURL& resourceUrl, |
178 Document& document, | 180 ExecutionContext& executionContext, |
179 String& errorMessage) { | 181 String& errorMessage) { |
180 if (!metadataSet.size()) | 182 if (!metadataSet.size()) |
181 return true; | 183 return true; |
182 | 184 |
183 HashAlgorithm strongestAlgorithm = HashAlgorithmSha256; | 185 HashAlgorithm strongestAlgorithm = HashAlgorithmSha256; |
184 for (const IntegrityMetadata& metadata : metadataSet) | 186 for (const IntegrityMetadata& metadata : metadataSet) |
185 strongestAlgorithm = | 187 strongestAlgorithm = |
186 getPrioritizedHashFunction(metadata.algorithm(), strongestAlgorithm); | 188 getPrioritizedHashFunction(metadata.algorithm(), strongestAlgorithm); |
187 | 189 |
188 DigestValue digest; | 190 DigestValue digest; |
189 for (const IntegrityMetadata& metadata : metadataSet) { | 191 for (const IntegrityMetadata& metadata : metadataSet) { |
190 if (metadata.algorithm() != strongestAlgorithm) | 192 if (metadata.algorithm() != strongestAlgorithm) |
191 continue; | 193 continue; |
192 | 194 |
193 digest.clear(); | 195 digest.clear(); |
194 bool digestSuccess = | 196 bool digestSuccess = |
195 computeDigest(metadata.algorithm(), content, size, digest); | 197 computeDigest(metadata.algorithm(), content, size, digest); |
196 | 198 |
197 if (digestSuccess) { | 199 if (digestSuccess) { |
198 Vector<char> hashVector; | 200 Vector<char> hashVector; |
199 base64Decode(metadata.digest(), hashVector); | 201 base64Decode(metadata.digest(), hashVector); |
200 DigestValue convertedHashVector; | 202 DigestValue convertedHashVector; |
201 convertedHashVector.append(reinterpret_cast<uint8_t*>(hashVector.data()), | 203 convertedHashVector.append(reinterpret_cast<uint8_t*>(hashVector.data()), |
202 hashVector.size()); | 204 hashVector.size()); |
203 | 205 |
204 if (DigestsEqual(digest, convertedHashVector)) { | 206 if (DigestsEqual(digest, convertedHashVector)) { |
205 UseCounter::count(document, | 207 UseCounter::count(&executionContext, |
206 UseCounter::SRIElementWithMatchingIntegrityAttribute); | 208 UseCounter::SRIElementWithMatchingIntegrityAttribute); |
207 return true; | 209 return true; |
208 } | 210 } |
209 } | 211 } |
210 } | 212 } |
211 | 213 |
212 digest.clear(); | 214 digest.clear(); |
213 if (computeDigest(HashAlgorithmSha256, content, size, digest)) { | 215 if (computeDigest(HashAlgorithmSha256, content, size, digest)) { |
214 // This message exposes the digest of the resource to the console. | 216 // This message exposes the digest of the resource to the console. |
215 // Because this is only to the console, that's okay for now, but we | 217 // Because this is only to the console, that's okay for now, but we |
216 // need to be very careful not to expose this in exceptions or | 218 // need to be very careful not to expose this in exceptions or |
217 // JavaScript, otherwise it risks exposing information about the | 219 // JavaScript, otherwise it risks exposing information about the |
218 // resource cross-origin. | 220 // resource cross-origin. |
219 errorMessage = | 221 errorMessage = |
220 "Failed to find a valid digest in the 'integrity' attribute for " | 222 "Failed to find a valid digest in the 'integrity' attribute for " |
221 "resource '" + | 223 "resource '" + |
222 resourceUrl.elidedString() + "' with computed SHA-256 integrity '" + | 224 resourceUrl.elidedString() + "' with computed SHA-256 integrity '" + |
223 digestToString(digest) + "'. The resource has been blocked."; | 225 digestToString(digest) + "'. The resource has been blocked."; |
224 } else { | 226 } else { |
225 errorMessage = | 227 errorMessage = |
226 "There was an error computing an integrity value for resource '" + | 228 "There was an error computing an integrity value for resource '" + |
227 resourceUrl.elidedString() + "'. The resource has been blocked."; | 229 resourceUrl.elidedString() + "'. The resource has been blocked."; |
228 } | 230 } |
229 UseCounter::count(document, | 231 UseCounter::count(&executionContext, |
230 UseCounter::SRIElementWithNonMatchingIntegrityAttribute); | 232 UseCounter::SRIElementWithNonMatchingIntegrityAttribute); |
231 return false; | 233 return false; |
232 } | 234 } |
233 | 235 |
234 // Before: | 236 // Before: |
235 // | 237 // |
236 // [algorithm]-[hash] | 238 // [algorithm]-[hash] |
237 // ^ ^ | 239 // ^ ^ |
238 // position end | 240 // position end |
239 // | 241 // |
(...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
310 } | 312 } |
311 | 313 |
312 SubresourceIntegrity::IntegrityParseResult | 314 SubresourceIntegrity::IntegrityParseResult |
313 SubresourceIntegrity::parseIntegrityAttribute( | 315 SubresourceIntegrity::parseIntegrityAttribute( |
314 const WTF::String& attribute, | 316 const WTF::String& attribute, |
315 IntegrityMetadataSet& metadataSet) { | 317 IntegrityMetadataSet& metadataSet) { |
316 return parseIntegrityAttribute(attribute, metadataSet, nullptr); | 318 return parseIntegrityAttribute(attribute, metadataSet, nullptr); |
317 } | 319 } |
318 | 320 |
319 SubresourceIntegrity::IntegrityParseResult | 321 SubresourceIntegrity::IntegrityParseResult |
320 SubresourceIntegrity::parseIntegrityAttribute(const WTF::String& attribute, | 322 SubresourceIntegrity::parseIntegrityAttribute( |
321 IntegrityMetadataSet& metadataSet, | 323 const WTF::String& attribute, |
322 Document* document) { | 324 IntegrityMetadataSet& metadataSet, |
| 325 ExecutionContext* executionContext) { |
323 Vector<UChar> characters; | 326 Vector<UChar> characters; |
324 attribute.stripWhiteSpace().appendTo(characters); | 327 attribute.stripWhiteSpace().appendTo(characters); |
325 const UChar* position = characters.data(); | 328 const UChar* position = characters.data(); |
326 const UChar* end = characters.end(); | 329 const UChar* end = characters.end(); |
327 const UChar* currentIntegrityEnd; | 330 const UChar* currentIntegrityEnd; |
328 | 331 |
329 metadataSet.clear(); | 332 metadataSet.clear(); |
330 bool error = false; | 333 bool error = false; |
331 | 334 |
332 // The integrity attribute takes the form: | 335 // The integrity attribute takes the form: |
(...skipping 11 matching lines...) Expand all Loading... |
344 // Algorithm parsing errors are non-fatal (the subresource should | 347 // Algorithm parsing errors are non-fatal (the subresource should |
345 // still be loaded) because strong hash algorithms should be used | 348 // still be loaded) because strong hash algorithms should be used |
346 // without fear of breaking older user agents that don't support | 349 // without fear of breaking older user agents that don't support |
347 // them. | 350 // them. |
348 AlgorithmParseResult parseResult = | 351 AlgorithmParseResult parseResult = |
349 parseAlgorithm(position, currentIntegrityEnd, algorithm); | 352 parseAlgorithm(position, currentIntegrityEnd, algorithm); |
350 if (parseResult == AlgorithmUnknown) { | 353 if (parseResult == AlgorithmUnknown) { |
351 // Unknown hash algorithms are treated as if they're not present, | 354 // Unknown hash algorithms are treated as if they're not present, |
352 // and thus are not marked as an error, they're just skipped. | 355 // and thus are not marked as an error, they're just skipped. |
353 skipUntil<UChar, isASCIISpace>(position, end); | 356 skipUntil<UChar, isASCIISpace>(position, end); |
354 if (document) { | 357 if (executionContext) { |
355 logErrorToConsole("Error parsing 'integrity' attribute ('" + attribute + | 358 logErrorToConsole("Error parsing 'integrity' attribute ('" + attribute + |
356 "'). The specified hash algorithm must be one of " | 359 "'). The specified hash algorithm must be one of " |
357 "'sha256', 'sha384', or 'sha512'.", | 360 "'sha256', 'sha384', or 'sha512'.", |
358 *document); | 361 *executionContext); |
359 UseCounter::count( | 362 UseCounter::count( |
360 *document, UseCounter::SRIElementWithUnparsableIntegrityAttribute); | 363 executionContext, |
| 364 UseCounter::SRIElementWithUnparsableIntegrityAttribute); |
361 } | 365 } |
362 continue; | 366 continue; |
363 } | 367 } |
364 | 368 |
365 if (parseResult == AlgorithmUnparsable) { | 369 if (parseResult == AlgorithmUnparsable) { |
366 error = true; | 370 error = true; |
367 skipUntil<UChar, isASCIISpace>(position, end); | 371 skipUntil<UChar, isASCIISpace>(position, end); |
368 if (document) { | 372 if (executionContext) { |
369 logErrorToConsole("Error parsing 'integrity' attribute ('" + attribute + | 373 logErrorToConsole("Error parsing 'integrity' attribute ('" + attribute + |
370 "'). The hash algorithm must be one of 'sha256', " | 374 "'). The hash algorithm must be one of 'sha256', " |
371 "'sha384', or 'sha512', followed by a '-' " | 375 "'sha384', or 'sha512', followed by a '-' " |
372 "character.", | 376 "character.", |
373 *document); | 377 *executionContext); |
374 UseCounter::count( | 378 UseCounter::count( |
375 *document, UseCounter::SRIElementWithUnparsableIntegrityAttribute); | 379 executionContext, |
| 380 UseCounter::SRIElementWithUnparsableIntegrityAttribute); |
376 } | 381 } |
377 continue; | 382 continue; |
378 } | 383 } |
379 | 384 |
380 ASSERT(parseResult == AlgorithmValid); | 385 ASSERT(parseResult == AlgorithmValid); |
381 | 386 |
382 if (!parseDigest(position, currentIntegrityEnd, digest)) { | 387 if (!parseDigest(position, currentIntegrityEnd, digest)) { |
383 error = true; | 388 error = true; |
384 skipUntil<UChar, isASCIISpace>(position, end); | 389 skipUntil<UChar, isASCIISpace>(position, end); |
385 if (document) { | 390 if (executionContext) { |
386 logErrorToConsole( | 391 logErrorToConsole( |
387 "Error parsing 'integrity' attribute ('" + attribute + | 392 "Error parsing 'integrity' attribute ('" + attribute + |
388 "'). The digest must be a valid, base64-encoded value.", | 393 "'). The digest must be a valid, base64-encoded value.", |
389 *document); | 394 *executionContext); |
390 UseCounter::count( | 395 UseCounter::count( |
391 *document, UseCounter::SRIElementWithUnparsableIntegrityAttribute); | 396 executionContext, |
| 397 UseCounter::SRIElementWithUnparsableIntegrityAttribute); |
392 } | 398 } |
393 continue; | 399 continue; |
394 } | 400 } |
395 | 401 |
396 // The spec defines a space in the syntax for options, separated by a | 402 // The spec defines a space in the syntax for options, separated by a |
397 // '?' character followed by unbounded VCHARs, but no actual options | 403 // '?' character followed by unbounded VCHARs, but no actual options |
398 // have been defined yet. Thus, for forward compatibility, ignore any | 404 // have been defined yet. Thus, for forward compatibility, ignore any |
399 // options specified. | 405 // options specified. |
400 if (skipExactly<UChar>(position, end, '?')) { | 406 if (skipExactly<UChar>(position, end, '?')) { |
401 const UChar* begin = position; | 407 const UChar* begin = position; |
402 skipWhile<UChar, isValueCharacter>(position, end); | 408 skipWhile<UChar, isValueCharacter>(position, end); |
403 if (begin != position && document) | 409 if (begin != position && executionContext) { |
404 logErrorToConsole( | 410 logErrorToConsole( |
405 "Ignoring unrecogized 'integrity' attribute option '" + | 411 "Ignoring unrecogized 'integrity' attribute option '" + |
406 String(begin, position - begin) + "'.", | 412 String(begin, position - begin) + "'.", |
407 *document); | 413 *executionContext); |
| 414 } |
408 } | 415 } |
409 | 416 |
410 IntegrityMetadata integrityMetadata(digest, algorithm); | 417 IntegrityMetadata integrityMetadata(digest, algorithm); |
411 metadataSet.add(integrityMetadata.toPair()); | 418 metadataSet.add(integrityMetadata.toPair()); |
412 } | 419 } |
413 | 420 |
414 if (metadataSet.size() == 0 && error) | 421 if (metadataSet.size() == 0 && error) |
415 return IntegrityParseNoValidResult; | 422 return IntegrityParseNoValidResult; |
416 | 423 |
417 return IntegrityParseValidResult; | 424 return IntegrityParseValidResult; |
418 } | 425 } |
419 | 426 |
420 } // namespace blink | 427 } // namespace blink |
OLD | NEW |