| 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 "config.h" | 5 #include "config.h" |
| 6 #include "core/frame/SubresourceIntegrity.h" | 6 #include "core/frame/SubresourceIntegrity.h" |
| 7 | 7 |
| 8 #include "core/HTMLNames.h" | 8 #include "core/HTMLNames.h" |
| 9 #include "core/dom/Document.h" | 9 #include "core/dom/Document.h" |
| 10 #include "core/dom/Element.h" | 10 #include "core/dom/Element.h" |
| (...skipping 17 matching lines...) Expand all Loading... |
| 28 namespace blink { | 28 namespace blink { |
| 29 | 29 |
| 30 // FIXME: This should probably use common functions with ContentSecurityPolicy. | 30 // FIXME: This should probably use common functions with ContentSecurityPolicy. |
| 31 static bool isIntegrityCharacter(UChar c) | 31 static bool isIntegrityCharacter(UChar c) |
| 32 { | 32 { |
| 33 // Check if it's a base64 encoded value. We're pretty loose here, as there's | 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. | 34 // not much risk in it, and it'll make it simpler for developers. |
| 35 return isASCIIAlphanumeric(c) || c == '_' || c == '-' || c == '+' || c == '/
' || c == '='; | 35 return isASCIIAlphanumeric(c) || c == '_' || c == '-' || c == '+' || c == '/
' || c == '='; |
| 36 } | 36 } |
| 37 | 37 |
| 38 static bool isTypeCharacter(UChar c) | 38 static bool isValueCharacter(UChar c) |
| 39 { | 39 { |
| 40 return isASCIIAlphanumeric(c) || c == '+' || c == '.' || c == '-'; | 40 // VCHAR per https://tools.ietf.org/html/rfc5234#appendix-B.1 |
| 41 return c >= 0x21 && c <= 0x7e; |
| 41 } | 42 } |
| 42 | 43 |
| 43 static void logErrorToConsole(const String& message, Document& document) | 44 static void logErrorToConsole(const String& message, Document& document) |
| 44 { | 45 { |
| 45 document.addConsoleMessage(ConsoleMessage::create(SecurityMessageSource, Err
orMessageLevel, message)); | 46 document.addConsoleMessage(ConsoleMessage::create(SecurityMessageSource, Err
orMessageLevel, message)); |
| 46 } | 47 } |
| 47 | 48 |
| 48 static bool DigestsEqual(const DigestValue& digest1, const DigestValue& digest2) | 49 static bool DigestsEqual(const DigestValue& digest1, const DigestValue& digest2) |
| 49 { | 50 { |
| 50 if (digest1.size() != digest2.size()) | 51 if (digest1.size() != digest2.size()) |
| 51 return false; | 52 return false; |
| 52 | 53 |
| 53 for (size_t i = 0; i < digest1.size(); i++) { | 54 for (size_t i = 0; i < digest1.size(); i++) { |
| 54 if (digest1[i] != digest2[i]) | 55 if (digest1[i] != digest2[i]) |
| 55 return false; | 56 return false; |
| 56 } | 57 } |
| 57 | 58 |
| 58 return true; | 59 return true; |
| 59 } | 60 } |
| 60 | 61 |
| 61 static String digestToString(const DigestValue& digest) | 62 static String digestToString(const DigestValue& digest) |
| 62 { | 63 { |
| 63 // We always output base64url encoded data, even though we use base64 intern
ally. | 64 // We always output base64url encoded data, even though we use base64 intern
ally. |
| 64 return base64URLEncode(reinterpret_cast<const char*>(digest.data()), digest.
size(), Base64DoNotInsertLFs); | 65 return base64URLEncode(reinterpret_cast<const char*>(digest.data()), digest.
size(), Base64DoNotInsertLFs); |
| 65 } | 66 } |
| 66 | 67 |
| 67 bool SubresourceIntegrity::CheckSubresourceIntegrity(const Element& element, con
st String& source, const KURL& resourceUrl, const String& resourceType, const Re
source& resource) | 68 bool SubresourceIntegrity::CheckSubresourceIntegrity(const Element& element, con
st String& source, const KURL& resourceUrl, const Resource& resource) |
| 68 { | 69 { |
| 69 if (!RuntimeEnabledFeatures::subresourceIntegrityEnabled()) | 70 if (!RuntimeEnabledFeatures::subresourceIntegrityEnabled()) |
| 70 return true; | 71 return true; |
| 71 | 72 |
| 72 if (!element.fastHasAttribute(HTMLNames::integrityAttr)) | 73 if (!element.fastHasAttribute(HTMLNames::integrityAttr)) |
| 73 return true; | 74 return true; |
| 74 | 75 |
| 75 Document& document = element.document(); | 76 Document& document = element.document(); |
| 76 | 77 |
| 77 if (!resource.isEligibleForIntegrityCheck(document.securityOrigin())) { | 78 if (!resource.isEligibleForIntegrityCheck(document.securityOrigin())) { |
| (...skipping 17 matching lines...) Expand all Loading... |
| 95 digest.clear(); | 96 digest.clear(); |
| 96 bool digestSuccess = computeDigest(metadata.algorithm, normalizedSource.
data(), normalizedSource.length(), digest); | 97 bool digestSuccess = computeDigest(metadata.algorithm, normalizedSource.
data(), normalizedSource.length(), digest); |
| 97 | 98 |
| 98 if (digestSuccess) { | 99 if (digestSuccess) { |
| 99 Vector<char> hashVector; | 100 Vector<char> hashVector; |
| 100 base64Decode(metadata.digest, hashVector); | 101 base64Decode(metadata.digest, hashVector); |
| 101 DigestValue convertedHashVector; | 102 DigestValue convertedHashVector; |
| 102 convertedHashVector.append(reinterpret_cast<uint8_t*>(hashVector.dat
a()), hashVector.size()); | 103 convertedHashVector.append(reinterpret_cast<uint8_t*>(hashVector.dat
a()), hashVector.size()); |
| 103 | 104 |
| 104 if (DigestsEqual(digest, convertedHashVector)) { | 105 if (DigestsEqual(digest, convertedHashVector)) { |
| 105 String& type = metadata.type; | 106 UseCounter::count(document, UseCounter::SRIElementWithMatchingIn
tegrityAttribute); |
| 106 if (!type.isEmpty() && !equalIgnoringCase(type, resourceType)) | 107 return true; |
| 107 UseCounter::count(document, UseCounter::SRIElementWithNonMat
chingIntegrityType); | |
| 108 else | |
| 109 return true; | |
| 110 } | 108 } |
| 111 } | 109 } |
| 112 } | 110 } |
| 113 | 111 |
| 114 if (computeDigest(HashAlgorithmSha256, normalizedSource.data(), normalizedSo
urce.length(), digest)) { | 112 if (computeDigest(HashAlgorithmSha256, normalizedSource.data(), normalizedSo
urce.length(), digest)) { |
| 115 // This message exposes the digest of the resource to the console. | 113 // This message exposes the digest of the resource to the console. |
| 116 // Because this is only to the console, that's okay for now, but we | 114 // Because this is only to the console, that's okay for now, but we |
| 117 // need to be very careful not to expose this in exceptions or | 115 // need to be very careful not to expose this in exceptions or |
| 118 // JavaScript, otherwise it risks exposing information about the | 116 // JavaScript, otherwise it risks exposing information about the |
| 119 // resource cross-origin. | 117 // resource cross-origin. |
| 120 logErrorToConsole("Failed to find a valid digest with matching content-t
ype in the 'integrity' attribute for resource '" + resourceUrl.elidedString() +
"' with computed SHA-256 integrity '" + digestToString(digest) + "'. The resourc
e has been blocked.", document); | 118 logErrorToConsole("Failed to find a valid digest in the 'integrity' attr
ibute for resource '" + resourceUrl.elidedString() + "' with computed SHA-256 in
tegrity '" + digestToString(digest) + "'. The resource has been blocked.", docum
ent); |
| 121 } else { | 119 } else { |
| 122 logErrorToConsole("There was an error computing an integrity value for r
esource '" + resourceUrl.elidedString() + "'. The resource has been blocked.", d
ocument); | 120 logErrorToConsole("There was an error computing an integrity value for r
esource '" + resourceUrl.elidedString() + "'. The resource has been blocked.", d
ocument); |
| 123 } | 121 } |
| 124 UseCounter::count(document, UseCounter::SRIElementWithNonMatchingIntegrityAt
tribute); | 122 UseCounter::count(document, UseCounter::SRIElementWithNonMatchingIntegrityAt
tribute); |
| 125 return false; | 123 return false; |
| 126 } | 124 } |
| 127 | 125 |
| 128 // Before: | 126 // Before: |
| 129 // | 127 // |
| 130 // [algorithm]-[hash] | 128 // [algorithm]-[hash] |
| (...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 196 if (position == begin || (position != end && *position != '?')) { | 194 if (position == begin || (position != end && *position != '?')) { |
| 197 digest = emptyString(); | 195 digest = emptyString(); |
| 198 return false; | 196 return false; |
| 199 } | 197 } |
| 200 | 198 |
| 201 // We accept base64url encoding, but normalize to "normal" base64 internally
: | 199 // We accept base64url encoding, but normalize to "normal" base64 internally
: |
| 202 digest = normalizeToBase64(String(begin, position - begin)); | 200 digest = normalizeToBase64(String(begin, position - begin)); |
| 203 return true; | 201 return true; |
| 204 } | 202 } |
| 205 | 203 |
| 206 | |
| 207 // Before: | |
| 208 // | |
| 209 // [algorithm]-[hash] OR [algorithm]-[hash]?[options] | |
| 210 // ^ ^ ^ | |
| 211 // position/end position end | |
| 212 // | |
| 213 // After (if successful: if the method returns false, we make no promises and th
e caller should exit early): | |
| 214 // | |
| 215 // [algorithm]-[hash] OR [algorithm]-[hash]?[options] | |
| 216 // ^ ^ | |
| 217 // position/end position/end | |
| 218 bool SubresourceIntegrity::parseMimeType(const UChar*& position, const UChar* en
d, String& type) | |
| 219 { | |
| 220 type = emptyString(); | |
| 221 | |
| 222 if (position == end) | |
| 223 return true; | |
| 224 | |
| 225 if (!skipToken<UChar>(position, end, "?ct=")) | |
| 226 return false; | |
| 227 | |
| 228 const UChar* begin = position; | |
| 229 skipWhile<UChar, isASCIIAlpha>(position, end); | |
| 230 if (position == end) | |
| 231 return false; | |
| 232 | |
| 233 if (!skipExactly<UChar>(position, end, '/')) | |
| 234 return false; | |
| 235 | |
| 236 if (position == end) | |
| 237 return false; | |
| 238 | |
| 239 skipWhile<UChar, isTypeCharacter>(position, end); | |
| 240 if (position != end) | |
| 241 return false; | |
| 242 | |
| 243 type = String(begin, position - begin); | |
| 244 return true; | |
| 245 } | |
| 246 | |
| 247 SubresourceIntegrity::IntegrityParseResult SubresourceIntegrity::parseIntegrityA
ttribute(const WTF::String& attribute, WTF::Vector<IntegrityMetadata>& metadataL
ist, Document& document) | 204 SubresourceIntegrity::IntegrityParseResult SubresourceIntegrity::parseIntegrityA
ttribute(const WTF::String& attribute, WTF::Vector<IntegrityMetadata>& metadataL
ist, Document& document) |
| 248 { | 205 { |
| 249 Vector<UChar> characters; | 206 Vector<UChar> characters; |
| 250 attribute.stripWhiteSpace().appendTo(characters); | 207 attribute.stripWhiteSpace().appendTo(characters); |
| 251 const UChar* position = characters.data(); | 208 const UChar* position = characters.data(); |
| 252 const UChar* end = characters.end(); | 209 const UChar* end = characters.end(); |
| 253 const UChar* currentIntegrityEnd; | 210 const UChar* currentIntegrityEnd; |
| 254 | 211 |
| 255 metadataList.clear(); | 212 metadataList.clear(); |
| 256 bool error = false; | 213 bool error = false; |
| 257 | 214 |
| 258 // The integrity attribute takes the form: | 215 // The integrity attribute takes the form: |
| 259 // *WSP hash-with-options *( 1*WSP hash-with-options ) *WSP / *WSP | 216 // *WSP hash-with-options *( 1*WSP hash-with-options ) *WSP / *WSP |
| 260 // To parse this, break on whitespace, parsing each algorithm/digest/mime | 217 // To parse this, break on whitespace, parsing each algorithm/digest/option |
| 261 // type in order. | 218 // in order. |
| 262 while (position < end) { | 219 while (position < end) { |
| 263 WTF::String digest; | 220 WTF::String digest; |
| 264 HashAlgorithm algorithm; | 221 HashAlgorithm algorithm; |
| 265 WTF::String type; | |
| 266 | 222 |
| 267 skipWhile<UChar, isASCIISpace>(position, end); | 223 skipWhile<UChar, isASCIISpace>(position, end); |
| 268 currentIntegrityEnd = position; | 224 currentIntegrityEnd = position; |
| 269 skipUntil<UChar, isASCIISpace>(currentIntegrityEnd, end); | 225 skipUntil<UChar, isASCIISpace>(currentIntegrityEnd, end); |
| 270 | 226 |
| 271 // Algorithm parsing errors are non-fatal (the subresource should | 227 // Algorithm parsing errors are non-fatal (the subresource should |
| 272 // still be loaded) because strong hash algorithms should be used | 228 // still be loaded) because strong hash algorithms should be used |
| 273 // without fear of breaking older user agents that don't support | 229 // without fear of breaking older user agents that don't support |
| 274 // them. | 230 // them. |
| 275 AlgorithmParseResult parseResult = parseAlgorithm(position, currentInteg
rityEnd, algorithm); | 231 AlgorithmParseResult parseResult = parseAlgorithm(position, currentInteg
rityEnd, algorithm); |
| (...skipping 17 matching lines...) Expand all Loading... |
| 293 ASSERT(parseResult == AlgorithmValid); | 249 ASSERT(parseResult == AlgorithmValid); |
| 294 | 250 |
| 295 if (!parseDigest(position, currentIntegrityEnd, digest)) { | 251 if (!parseDigest(position, currentIntegrityEnd, digest)) { |
| 296 logErrorToConsole("Error parsing 'integrity' attribute ('" + attribu
te + "'). The digest must be a valid, base64-encoded value.", document); | 252 logErrorToConsole("Error parsing 'integrity' attribute ('" + attribu
te + "'). The digest must be a valid, base64-encoded value.", document); |
| 297 error = true; | 253 error = true; |
| 298 skipUntil<UChar, isASCIISpace>(position, end); | 254 skipUntil<UChar, isASCIISpace>(position, end); |
| 299 UseCounter::count(document, UseCounter::SRIElementWithUnparsableInte
grityAttribute); | 255 UseCounter::count(document, UseCounter::SRIElementWithUnparsableInte
grityAttribute); |
| 300 continue; | 256 continue; |
| 301 } | 257 } |
| 302 | 258 |
| 303 if (!parseMimeType(position, currentIntegrityEnd, type)) { | 259 // The spec defines a space in the syntax for options, separated by a |
| 304 logErrorToConsole("Error parsing 'integrity' attribute ('" + attribu
te + "'). The content type could not be parsed.", document); | 260 // '?' character followed by unbounded VCHARs, but no actual options |
| 305 error = true; | 261 // have been defined yet. Thus, for forward compatibility, ignore any |
| 306 skipUntil<UChar, isASCIISpace>(position, end); | 262 // options specified. |
| 307 UseCounter::count(document, UseCounter::SRIElementWithUnparsableInte
grityAttribute); | 263 if (skipExactly<UChar>(position, end, '?')) { |
| 308 continue; | 264 const UChar* begin = position; |
| 265 skipWhile<UChar, isValueCharacter>(position, end); |
| 266 if (begin != position) |
| 267 logErrorToConsole("Ignoring unrecogized 'integrity' attribute op
tion '" + String(begin, position - begin) + "'.", document); |
| 309 } | 268 } |
| 310 | 269 |
| 311 IntegrityMetadata integrityMetadata = { | 270 IntegrityMetadata integrityMetadata = { |
| 312 digest, | 271 digest, |
| 313 algorithm, | 272 algorithm |
| 314 type | |
| 315 }; | 273 }; |
| 316 metadataList.append(integrityMetadata); | 274 metadataList.append(integrityMetadata); |
| 317 } | 275 } |
| 318 | 276 |
| 319 if (metadataList.size() == 0 && error) | 277 if (metadataList.size() == 0 && error) |
| 320 return IntegrityParseNoValidResult; | 278 return IntegrityParseNoValidResult; |
| 321 | 279 |
| 322 return IntegrityParseValidResult; | 280 return IntegrityParseValidResult; |
| 323 } | 281 } |
| 324 | 282 |
| 325 } // namespace blink | 283 } // namespace blink |
| OLD | NEW |