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 isOptionNameCharacter(UChar c) |
39 { | 39 { |
40 return isASCIIAlphanumeric(c) || c == '+' || c == '.' || c == '-'; | 40 return isASCIIAlphanumeric(c) || c == '-'; |
41 } | |
42 | |
43 static bool isOptionValueCharacter(UChar c) | |
44 { | |
45 return isASCIIAlphanumeric(c) || c == '!' || c == '#' || c == '$' || c == '% ' || c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' || c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~' || c == '/'; | |
Mike West
2015/05/11 03:25:29
This is a strangely arbitrary list of characters.
jww
2015/05/11 05:39:07
Fair point. As per fmarier's suggestion in https:/
| |
41 } | 46 } |
42 | 47 |
43 static void logErrorToConsole(const String& message, Document& document) | 48 static void logErrorToConsole(const String& message, Document& document) |
44 { | 49 { |
45 document.addConsoleMessage(ConsoleMessage::create(SecurityMessageSource, Err orMessageLevel, message)); | 50 document.addConsoleMessage(ConsoleMessage::create(SecurityMessageSource, Err orMessageLevel, message)); |
46 } | 51 } |
47 | 52 |
48 static bool DigestsEqual(const DigestValue& digest1, const DigestValue& digest2) | 53 static bool DigestsEqual(const DigestValue& digest1, const DigestValue& digest2) |
49 { | 54 { |
50 if (digest1.size() != digest2.size()) | 55 if (digest1.size() != digest2.size()) |
51 return false; | 56 return false; |
52 | 57 |
53 for (size_t i = 0; i < digest1.size(); i++) { | 58 for (size_t i = 0; i < digest1.size(); i++) { |
54 if (digest1[i] != digest2[i]) | 59 if (digest1[i] != digest2[i]) |
55 return false; | 60 return false; |
56 } | 61 } |
57 | 62 |
58 return true; | 63 return true; |
59 } | 64 } |
60 | 65 |
61 static String digestToString(const DigestValue& digest) | 66 static String digestToString(const DigestValue& digest) |
62 { | 67 { |
63 // We always output base64url encoded data, even though we use base64 intern ally. | 68 // 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); | 69 return base64URLEncode(reinterpret_cast<const char*>(digest.data()), digest. size(), Base64DoNotInsertLFs); |
65 } | 70 } |
66 | 71 |
67 bool SubresourceIntegrity::CheckSubresourceIntegrity(const Element& element, con st String& source, const KURL& resourceUrl, const String& resourceType, const Re source& resource) | 72 bool SubresourceIntegrity::CheckSubresourceIntegrity(const Element& element, con st String& source, const KURL& resourceUrl, const Resource& resource) |
68 { | 73 { |
69 if (!RuntimeEnabledFeatures::subresourceIntegrityEnabled()) | 74 if (!RuntimeEnabledFeatures::subresourceIntegrityEnabled()) |
70 return true; | 75 return true; |
71 | 76 |
72 if (!element.fastHasAttribute(HTMLNames::integrityAttr)) | 77 if (!element.fastHasAttribute(HTMLNames::integrityAttr)) |
73 return true; | 78 return true; |
74 | 79 |
75 Document& document = element.document(); | 80 Document& document = element.document(); |
76 | 81 |
77 if (!resource.isEligibleForIntegrityCheck(document.securityOrigin())) { | 82 if (!resource.isEligibleForIntegrityCheck(document.securityOrigin())) { |
(...skipping 16 matching lines...) Expand all Loading... | |
94 for (IntegrityMetadata& metadata : metadataList) { | 99 for (IntegrityMetadata& metadata : metadataList) { |
95 digest.clear(); | 100 digest.clear(); |
96 bool digestSuccess = computeDigest(metadata.algorithm, normalizedSource. data(), normalizedSource.length(), digest); | 101 bool digestSuccess = computeDigest(metadata.algorithm, normalizedSource. data(), normalizedSource.length(), digest); |
97 | 102 |
98 if (digestSuccess) { | 103 if (digestSuccess) { |
99 Vector<char> hashVector; | 104 Vector<char> hashVector; |
100 base64Decode(metadata.digest, hashVector); | 105 base64Decode(metadata.digest, hashVector); |
101 DigestValue convertedHashVector; | 106 DigestValue convertedHashVector; |
102 convertedHashVector.append(reinterpret_cast<uint8_t*>(hashVector.dat a()), hashVector.size()); | 107 convertedHashVector.append(reinterpret_cast<uint8_t*>(hashVector.dat a()), hashVector.size()); |
103 | 108 |
104 if (DigestsEqual(digest, convertedHashVector)) { | 109 if (DigestsEqual(digest, convertedHashVector)) |
105 String& type = metadata.type; | 110 return true; |
106 if (!type.isEmpty() && !equalIgnoringCase(type, resourceType)) | |
107 UseCounter::count(document, UseCounter::SRIElementWithNonMat chingIntegrityType); | |
108 else | |
109 return true; | |
110 } | |
111 } | 111 } |
112 } | 112 } |
113 | 113 |
114 if (computeDigest(HashAlgorithmSha256, normalizedSource.data(), normalizedSo urce.length(), digest)) { | 114 if (computeDigest(HashAlgorithmSha256, normalizedSource.data(), normalizedSo urce.length(), digest)) { |
115 // This message exposes the digest of the resource to the console. | 115 // 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 | 116 // 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 | 117 // need to be very careful not to expose this in exceptions or |
118 // JavaScript, otherwise it risks exposing information about the | 118 // JavaScript, otherwise it risks exposing information about the |
119 // resource cross-origin. | 119 // 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); | 120 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 { | 121 } else { |
122 logErrorToConsole("There was an error computing an integrity value for r esource '" + resourceUrl.elidedString() + "'. The resource has been blocked.", d ocument); | 122 logErrorToConsole("There was an error computing an integrity value for r esource '" + resourceUrl.elidedString() + "'. The resource has been blocked.", d ocument); |
123 } | 123 } |
124 UseCounter::count(document, UseCounter::SRIElementWithNonMatchingIntegrityAt tribute); | 124 UseCounter::count(document, UseCounter::SRIElementWithNonMatchingIntegrityAt tribute); |
125 return false; | 125 return false; |
126 } | 126 } |
127 | 127 |
128 // Before: | 128 // Before: |
129 // | 129 // |
130 // [algorithm]-[hash] | 130 // [algorithm]-[hash] |
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
196 if (position == begin || (position != end && *position != '?')) { | 196 if (position == begin || (position != end && *position != '?')) { |
197 digest = emptyString(); | 197 digest = emptyString(); |
198 return false; | 198 return false; |
199 } | 199 } |
200 | 200 |
201 // We accept base64url encoding, but normalize to "normal" base64 internally : | 201 // We accept base64url encoding, but normalize to "normal" base64 internally : |
202 digest = normalizeToBase64(String(begin, position - begin)); | 202 digest = normalizeToBase64(String(begin, position - begin)); |
203 return true; | 203 return true; |
204 } | 204 } |
205 | 205 |
206 | |
207 // Before: | 206 // Before: |
208 // | 207 // |
209 // [algorithm]-[hash] OR [algorithm]-[hash]?[options] | 208 // [algorithm]-[hash] OR [algorithm]-[hash]?[name]=[value] OR [algorithm]-[hash]?[name]=[value]?[rest of options] |
Mike West
2015/05/11 03:25:29
This is strange syntax. I'm not going to bikeshed
jww
2015/05/11 05:39:07
Bikeshedding irrelevant in my latest pull request
| |
210 // ^ ^ ^ | 209 // ^ ^ ^ ^ ^ |
211 // position/end position end | 210 // position/end position end position end |
212 // | 211 // |
213 // After (if successful: if the method returns false, we make no promises and th e caller should exit early): | 212 // After (if successful: if the method returns false, we make no promises and th e caller should exit early): |
214 // | 213 // |
215 // [algorithm]-[hash] OR [algorithm]-[hash]?[options] | 214 // [algorithm]-[hash] OR [algorithm]-[hash]?[name]=[value] OR [algorithm]-[hash]?[name]=[value]?[rest of options] |
216 // ^ ^ | 215 // ^ ^ ^ ^ |
217 // position/end position/end | 216 // position/end position/end position end |
218 bool SubresourceIntegrity::parseMimeType(const UChar*& position, const UChar* en d, String& type) | 217 bool SubresourceIntegrity::parseOption(const UChar*& position, const UChar* end, String& name, String& value) |
219 { | 218 { |
220 type = emptyString(); | 219 name = emptyString(); |
220 value = emptyString(); | |
221 | 221 |
222 if (position == end) | 222 if (position == end) |
223 return true; | 223 return true; |
224 | 224 |
225 if (!skipToken<UChar>(position, end, "?ct=")) | 225 if (!skipExactly<UChar>(position, end, '?')) |
226 return false; | 226 return false; |
227 | 227 |
228 const UChar* begin = position; | 228 const UChar* begin = position; |
229 skipWhile<UChar, isASCIIAlpha>(position, end); | 229 skipWhile<UChar, isOptionNameCharacter>(position, end); |
230 if (position == end) | 230 if (position == begin || position == end) |
231 return false; | 231 return false; |
232 | 232 |
233 if (!skipExactly<UChar>(position, end, '/')) | 233 name = String(begin, position - begin); |
Mike West
2015/05/11 03:25:29
This means `name` will be set to some value, even
jww
2015/05/11 05:39:07
Also irrelevant given the broader changes, but in
| |
234 | |
235 if (!skipExactly<UChar>(position, end, '=')) | |
234 return false; | 236 return false; |
235 | 237 |
236 if (position == end) | 238 begin = position; |
237 return false; | 239 skipWhile<UChar, isOptionValueCharacter>(position, end); |
238 | 240 |
239 skipWhile<UChar, isTypeCharacter>(position, end); | 241 value = String(begin, position - begin); |
240 if (position != end) | |
241 return false; | |
242 | 242 |
243 type = String(begin, position - begin); | 243 if (position == end || *position == '?') |
Mike West
2015/05/11 03:25:29
Nit: Please ASSERT that `position` is in-range bef
jww
2015/05/11 05:39:07
Also irrelevant now.
| |
244 return true; | 244 return true; |
245 | |
246 return false; | |
245 } | 247 } |
246 | 248 |
247 SubresourceIntegrity::IntegrityParseResult SubresourceIntegrity::parseIntegrityA ttribute(const WTF::String& attribute, WTF::Vector<IntegrityMetadata>& metadataL ist, Document& document) | 249 SubresourceIntegrity::IntegrityParseResult SubresourceIntegrity::parseIntegrityA ttribute(const WTF::String& attribute, WTF::Vector<IntegrityMetadata>& metadataL ist, Document& document) |
248 { | 250 { |
249 Vector<UChar> characters; | 251 Vector<UChar> characters; |
250 attribute.stripWhiteSpace().appendTo(characters); | 252 attribute.stripWhiteSpace().appendTo(characters); |
251 const UChar* position = characters.data(); | 253 const UChar* position = characters.data(); |
252 const UChar* end = characters.end(); | 254 const UChar* end = characters.end(); |
253 const UChar* currentIntegrityEnd; | 255 const UChar* currentIntegrityEnd; |
254 | 256 |
255 metadataList.clear(); | 257 metadataList.clear(); |
256 bool error = false; | 258 bool error = false; |
257 | 259 |
258 // The integrity attribute takes the form: | 260 // The integrity attribute takes the form: |
259 // *WSP hash-with-options *( 1*WSP hash-with-options ) *WSP / *WSP | 261 // *WSP hash-with-options *( 1*WSP hash-with-options ) *WSP / *WSP |
260 // To parse this, break on whitespace, parsing each algorithm/digest/mime | 262 // To parse this, break on whitespace, parsing each algorithm/digest/option |
261 // type in order. | 263 // in order. |
262 while (position < end) { | 264 while (position < end) { |
263 WTF::String digest; | 265 WTF::String digest; |
264 HashAlgorithm algorithm; | 266 HashAlgorithm algorithm; |
265 WTF::String type; | |
266 | 267 |
267 skipWhile<UChar, isASCIISpace>(position, end); | 268 skipWhile<UChar, isASCIISpace>(position, end); |
268 currentIntegrityEnd = position; | 269 currentIntegrityEnd = position; |
269 skipUntil<UChar, isASCIISpace>(currentIntegrityEnd, end); | 270 skipUntil<UChar, isASCIISpace>(currentIntegrityEnd, end); |
270 | 271 |
271 // Algorithm parsing errors are non-fatal (the subresource should | 272 // Algorithm parsing errors are non-fatal (the subresource should |
272 // still be loaded) because strong hash algorithms should be used | 273 // still be loaded) because strong hash algorithms should be used |
273 // without fear of breaking older user agents that don't support | 274 // without fear of breaking older user agents that don't support |
274 // them. | 275 // them. |
275 AlgorithmParseResult parseResult = parseAlgorithm(position, currentInteg rityEnd, algorithm); | 276 AlgorithmParseResult parseResult = parseAlgorithm(position, currentInteg rityEnd, algorithm); |
(...skipping 17 matching lines...) Expand all Loading... | |
293 ASSERT(parseResult == AlgorithmValid); | 294 ASSERT(parseResult == AlgorithmValid); |
294 | 295 |
295 if (!parseDigest(position, currentIntegrityEnd, digest)) { | 296 if (!parseDigest(position, currentIntegrityEnd, digest)) { |
296 logErrorToConsole("Error parsing 'integrity' attribute ('" + attribu te + "'). The digest must be a valid, base64-encoded value.", document); | 297 logErrorToConsole("Error parsing 'integrity' attribute ('" + attribu te + "'). The digest must be a valid, base64-encoded value.", document); |
297 error = true; | 298 error = true; |
298 skipUntil<UChar, isASCIISpace>(position, end); | 299 skipUntil<UChar, isASCIISpace>(position, end); |
299 UseCounter::count(document, UseCounter::SRIElementWithUnparsableInte grityAttribute); | 300 UseCounter::count(document, UseCounter::SRIElementWithUnparsableInte grityAttribute); |
300 continue; | 301 continue; |
301 } | 302 } |
302 | 303 |
303 if (!parseMimeType(position, currentIntegrityEnd, type)) { | 304 bool unparsableOptions = false; |
304 logErrorToConsole("Error parsing 'integrity' attribute ('" + attribu te + "'). The content type could not be parsed.", document); | 305 while (*position == '?') { |
305 error = true; | 306 String name, value; |
306 skipUntil<UChar, isASCIISpace>(position, end); | 307 if (!parseOption(position, currentIntegrityEnd, name, value)) { |
307 UseCounter::count(document, UseCounter::SRIElementWithUnparsableInte grityAttribute); | 308 logErrorToConsole("Error parsing 'integrity' attribute ('" + att ribute + "'). The options could not be parsed.", document); |
308 continue; | 309 unparsableOptions = true; |
310 error = true; | |
311 skipUntil<UChar, isASCIISpace>(position, end); | |
312 UseCounter::count(document, UseCounter::SRIElementWithUnparsable IntegrityAttribute); | |
313 break; | |
314 } | |
315 | |
316 logErrorToConsole("Ignoring unrecogized 'integrity' attribute option '" + name + "' with value '" + value + "'.", document); | |
Mike West
2015/05/11 03:25:29
This would end up logging a lot if/when options be
jww
2015/05/11 05:39:07
My new change logs just once the entire unknown op
| |
309 } | 317 } |
310 | 318 |
311 IntegrityMetadata integrityMetadata = { | 319 if (!unparsableOptions) { |
Mike West
2015/05/11 03:25:29
Why do you need a separate boolean here? Would che
jww
2015/05/11 05:39:07
Alas, it wouldn't have been :-( Error tracks if an
| |
312 digest, | 320 IntegrityMetadata integrityMetadata = { |
313 algorithm, | 321 digest, |
314 type | 322 algorithm |
315 }; | 323 }; |
316 metadataList.append(integrityMetadata); | 324 metadataList.append(integrityMetadata); |
325 } | |
317 } | 326 } |
318 | 327 |
319 if (metadataList.size() == 0 && error) | 328 if (metadataList.size() == 0 && error) |
320 return IntegrityParseNoValidResult; | 329 return IntegrityParseNoValidResult; |
321 | 330 |
322 return IntegrityParseValidResult; | 331 return IntegrityParseValidResult; |
323 } | 332 } |
324 | 333 |
325 } // namespace blink | 334 } // namespace blink |
OLD | NEW |