Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(490)

Side by Side Diff: third_party/WebKit/Source/core/fetch/CrossOriginAccessControl.cpp

Issue 2584423002: Loading: move core/fetch to platform/loader/fetch (Closed)
Patch Set: another try Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 /*
2 * Copyright (C) 2008 Apple Inc. All Rights Reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 *
25 */
26
27 #include "core/fetch/CrossOriginAccessControl.h"
28
29 #include "core/fetch/FetchUtils.h"
30 #include "core/fetch/Resource.h"
31 #include "core/fetch/ResourceLoaderOptions.h"
32 #include "platform/network/HTTPParsers.h"
33 #include "platform/network/ResourceRequest.h"
34 #include "platform/network/ResourceResponse.h"
35 #include "platform/weborigin/SchemeRegistry.h"
36 #include "platform/weborigin/SecurityOrigin.h"
37 #include "wtf/PtrUtil.h"
38 #include "wtf/Threading.h"
39 #include "wtf/text/AtomicString.h"
40 #include "wtf/text/StringBuilder.h"
41 #include <algorithm>
42 #include <memory>
43
44 namespace blink {
45
46 bool isOnAccessControlResponseHeaderWhitelist(const String& name) {
47 DEFINE_THREAD_SAFE_STATIC_LOCAL(
48 HTTPHeaderSet, allowedCrossOriginResponseHeaders,
49 (new HTTPHeaderSet({
50 "cache-control", "content-language", "content-type", "expires",
51 "last-modified", "pragma",
52 })));
53 return allowedCrossOriginResponseHeaders.contains(name);
54 }
55
56 // Fetch API Spec: https://fetch.spec.whatwg.org/#cors-preflight-fetch-0
57 static AtomicString createAccessControlRequestHeadersHeader(
58 const HTTPHeaderMap& headers) {
59 Vector<String> filteredHeaders;
60 for (const auto& header : headers) {
61 if (FetchUtils::isSimpleHeader(header.key, header.value)) {
62 // Exclude simple headers.
63 continue;
64 }
65 if (equalIgnoringCase(header.key, "referer")) {
66 // When the request is from a Worker, referrer header was added by
67 // WorkerThreadableLoader. But it should not be added to
68 // Access-Control-Request-Headers header.
69 continue;
70 }
71 filteredHeaders.push_back(header.key.lower());
72 }
73 if (!filteredHeaders.size())
74 return nullAtom;
75
76 // Sort header names lexicographically.
77 std::sort(filteredHeaders.begin(), filteredHeaders.end(),
78 WTF::codePointCompareLessThan);
79 StringBuilder headerBuffer;
80 for (const String& header : filteredHeaders) {
81 if (!headerBuffer.isEmpty())
82 headerBuffer.append(",");
83 headerBuffer.append(header);
84 }
85
86 return AtomicString(headerBuffer.toString());
87 }
88
89 ResourceRequest createAccessControlPreflightRequest(
90 const ResourceRequest& request) {
91 const KURL& requestURL = request.url();
92
93 DCHECK(requestURL.user().isEmpty());
94 DCHECK(requestURL.pass().isEmpty());
95
96 ResourceRequest preflightRequest(requestURL);
97 preflightRequest.setAllowStoredCredentials(false);
98 preflightRequest.setHTTPMethod(HTTPNames::OPTIONS);
99 preflightRequest.setHTTPHeaderField(HTTPNames::Access_Control_Request_Method,
100 AtomicString(request.httpMethod()));
101 preflightRequest.setPriority(request.priority());
102 preflightRequest.setRequestContext(request.requestContext());
103 preflightRequest.setSkipServiceWorker(WebURLRequest::SkipServiceWorker::All);
104
105 if (request.isExternalRequest()) {
106 preflightRequest.setHTTPHeaderField(
107 HTTPNames::Access_Control_Request_External, "true");
108 }
109
110 AtomicString requestHeaders =
111 createAccessControlRequestHeadersHeader(request.httpHeaderFields());
112 if (requestHeaders != nullAtom) {
113 preflightRequest.setHTTPHeaderField(
114 HTTPNames::Access_Control_Request_Headers, requestHeaders);
115 }
116
117 return preflightRequest;
118 }
119
120 static bool isOriginSeparator(UChar ch) {
121 return isASCIISpace(ch) || ch == ',';
122 }
123
124 static bool isInterestingStatusCode(int statusCode) {
125 // Predicate that gates what status codes should be included in console error
126 // messages for responses containing no access control headers.
127 return statusCode >= 400;
128 }
129
130 static void appendOriginDeniedMessage(StringBuilder& builder,
131 const SecurityOrigin* securityOrigin) {
132 builder.append(" Origin '");
133 builder.append(securityOrigin->toString());
134 builder.append("' is therefore not allowed access.");
135 }
136
137 static void appendNoCORSInformationalMessage(
138 StringBuilder& builder,
139 WebURLRequest::RequestContext context) {
140 if (context != WebURLRequest::RequestContextFetch)
141 return;
142 builder.append(
143 " Have the server send the header with a valid value, or, if an "
144 "opaque response serves your needs, set the request's mode to "
145 "'no-cors' to fetch the resource with CORS disabled.");
146 }
147
148 CrossOriginAccessControl::AccessStatus CrossOriginAccessControl::checkAccess(
149 const ResourceResponse& response,
150 StoredCredentials includeCredentials,
151 const SecurityOrigin* securityOrigin) {
152 DEFINE_THREAD_SAFE_STATIC_LOCAL(
153 AtomicString, allowOriginHeaderName,
154 (new AtomicString("access-control-allow-origin")));
155 DEFINE_THREAD_SAFE_STATIC_LOCAL(
156 AtomicString, allowCredentialsHeaderName,
157 (new AtomicString("access-control-allow-credentials")));
158 DEFINE_THREAD_SAFE_STATIC_LOCAL(
159 AtomicString, allowSuboriginHeaderName,
160 (new AtomicString("access-control-allow-suborigin")));
161
162 int statusCode = response.httpStatusCode();
163 if (!statusCode)
164 return kInvalidResponse;
165
166 const AtomicString& allowOriginHeaderValue =
167 response.httpHeaderField(allowOriginHeaderName);
168
169 // Check Suborigins, unless the Access-Control-Allow-Origin is '*', which
170 // implies that all Suborigins are okay as well.
171 if (securityOrigin->hasSuborigin() && allowOriginHeaderValue != starAtom) {
172 const AtomicString& allowSuboriginHeaderValue =
173 response.httpHeaderField(allowSuboriginHeaderName);
174 AtomicString atomicSuboriginName(securityOrigin->suborigin()->name());
175 if (allowSuboriginHeaderValue != starAtom &&
176 allowSuboriginHeaderValue != atomicSuboriginName) {
177 return kSubOriginMismatch;
178 }
179 }
180
181 if (allowOriginHeaderValue == starAtom) {
182 // A wildcard Access-Control-Allow-Origin can not be used if credentials are
183 // to be sent, even with Access-Control-Allow-Credentials set to true.
184 if (includeCredentials == DoNotAllowStoredCredentials)
185 return kAccessAllowed;
186 if (response.isHTTP()) {
187 return kWildcardOriginNotAllowed;
188 }
189 } else if (allowOriginHeaderValue != securityOrigin->toAtomicString()) {
190 if (allowOriginHeaderValue.isNull())
191 return kMissingAllowOriginHeader;
192 if (allowOriginHeaderValue.getString().find(isOriginSeparator, 0) !=
193 kNotFound) {
194 return kMultipleAllowOriginValues;
195 }
196 KURL headerOrigin(KURL(), allowOriginHeaderValue);
197 if (!headerOrigin.isValid())
198 return kInvalidAllowOriginValue;
199
200 return kAllowOriginMismatch;
201 }
202
203 if (includeCredentials == AllowStoredCredentials) {
204 const AtomicString& allowCredentialsHeaderValue =
205 response.httpHeaderField(allowCredentialsHeaderName);
206 if (allowCredentialsHeaderValue != "true") {
207 return kDisallowCredentialsNotSetToTrue;
208 }
209 }
210 return kAccessAllowed;
211 }
212
213 void CrossOriginAccessControl::accessControlErrorString(
214 StringBuilder& builder,
215 CrossOriginAccessControl::AccessStatus status,
216 const ResourceResponse& response,
217 const SecurityOrigin* securityOrigin,
218 WebURLRequest::RequestContext context) {
219 DEFINE_THREAD_SAFE_STATIC_LOCAL(
220 AtomicString, allowOriginHeaderName,
221 (new AtomicString("access-control-allow-origin")));
222 DEFINE_THREAD_SAFE_STATIC_LOCAL(
223 AtomicString, allowCredentialsHeaderName,
224 (new AtomicString("access-control-allow-credentials")));
225 DEFINE_THREAD_SAFE_STATIC_LOCAL(
226 AtomicString, allowSuboriginHeaderName,
227 (new AtomicString("access-control-allow-suborigin")));
228
229 switch (status) {
230 case kInvalidResponse: {
231 builder.append("Invalid response.");
232 appendOriginDeniedMessage(builder, securityOrigin);
233 return;
234 }
235 case kSubOriginMismatch: {
236 const AtomicString& allowSuboriginHeaderValue =
237 response.httpHeaderField(allowSuboriginHeaderName);
238 builder.append(
239 "The 'Access-Control-Allow-Suborigin' header has a value '");
240 builder.append(allowSuboriginHeaderValue);
241 builder.append("' that is not equal to the supplied suborigin.");
242 appendOriginDeniedMessage(builder, securityOrigin);
243 return;
244 }
245 case kWildcardOriginNotAllowed: {
246 builder.append(
247 "The value of the 'Access-Control-Allow-Origin' header in the "
248 "response must not be the wildcard '*' when the request's "
249 "credentials mode is 'include'.");
250 appendOriginDeniedMessage(builder, securityOrigin);
251 if (context == WebURLRequest::RequestContextXMLHttpRequest) {
252 builder.append(
253 " The credentials mode of requests initiated by the "
254 "XMLHttpRequest is controlled by the withCredentials attribute.");
255 }
256 return;
257 }
258 case kMissingAllowOriginHeader: {
259 builder.append(
260 "No 'Access-Control-Allow-Origin' header is present on the requested "
261 "resource.");
262 appendOriginDeniedMessage(builder, securityOrigin);
263 int statusCode = response.httpStatusCode();
264 if (isInterestingStatusCode(statusCode)) {
265 builder.append(" The response had HTTP status code ");
266 builder.append(String::number(statusCode));
267 builder.append('.');
268 }
269 if (context == WebURLRequest::RequestContextFetch) {
270 builder.append(
271 " If an opaque response serves your needs, set the request's mode "
272 "to 'no-cors' to fetch the resource with CORS disabled.");
273 }
274 return;
275 }
276 case kMultipleAllowOriginValues: {
277 const AtomicString& allowOriginHeaderValue =
278 response.httpHeaderField(allowOriginHeaderName);
279 builder.append(
280 "The 'Access-Control-Allow-Origin' header contains multiple values "
281 "'");
282 builder.append(allowOriginHeaderValue);
283 builder.append("', but only one is allowed.");
284 appendOriginDeniedMessage(builder, securityOrigin);
285 appendNoCORSInformationalMessage(builder, context);
286 return;
287 }
288 case kInvalidAllowOriginValue: {
289 const AtomicString& allowOriginHeaderValue =
290 response.httpHeaderField(allowOriginHeaderName);
291 builder.append(
292 "The 'Access-Control-Allow-Origin' header contains the invalid "
293 "value '");
294 builder.append(allowOriginHeaderValue);
295 builder.append("'.");
296 appendOriginDeniedMessage(builder, securityOrigin);
297 appendNoCORSInformationalMessage(builder, context);
298 return;
299 }
300 case kAllowOriginMismatch: {
301 const AtomicString& allowOriginHeaderValue =
302 response.httpHeaderField(allowOriginHeaderName);
303 builder.append("The 'Access-Control-Allow-Origin' header has a value '");
304 builder.append(allowOriginHeaderValue);
305 builder.append("' that is not equal to the supplied origin.");
306 appendOriginDeniedMessage(builder, securityOrigin);
307 appendNoCORSInformationalMessage(builder, context);
308 return;
309 }
310 case kDisallowCredentialsNotSetToTrue: {
311 const AtomicString& allowCredentialsHeaderValue =
312 response.httpHeaderField(allowCredentialsHeaderName);
313 builder.append(
314 "The value of the 'Access-Control-Allow-Credentials' header in "
315 "the response is '");
316 builder.append(allowCredentialsHeaderValue);
317 builder.append(
318 "' which must "
319 "be 'true' when the request's credentials mode is 'include'.");
320 appendOriginDeniedMessage(builder, securityOrigin);
321 if (context == WebURLRequest::RequestContextXMLHttpRequest) {
322 builder.append(
323 " The credentials mode of requests initiated by the "
324 "XMLHttpRequest is controlled by the withCredentials attribute.");
325 }
326 return;
327 }
328 default:
329 NOTREACHED();
330 }
331 }
332
333 CrossOriginAccessControl::PreflightStatus
334 CrossOriginAccessControl::checkPreflight(const ResourceResponse& response) {
335 // CORS preflight with 3XX is considered network error in
336 // Fetch API Spec: https://fetch.spec.whatwg.org/#cors-preflight-fetch
337 // CORS Spec: http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0
338 // https://crbug.com/452394
339 int statusCode = response.httpStatusCode();
340 if (!FetchUtils::isOkStatus(statusCode))
341 return kPreflightInvalidStatus;
342
343 return kPreflightSuccess;
344 }
345
346 CrossOriginAccessControl::PreflightStatus
347 CrossOriginAccessControl::checkExternalPreflight(
348 const ResourceResponse& response) {
349 AtomicString result =
350 response.httpHeaderField(HTTPNames::Access_Control_Allow_External);
351 if (result.isNull())
352 return kPreflightMissingAllowExternal;
353 if (!equalIgnoringCase(result, "true"))
354 return kPreflightInvalidAllowExternal;
355 return kPreflightSuccess;
356 }
357
358 void CrossOriginAccessControl::preflightErrorString(
359 StringBuilder& builder,
360 CrossOriginAccessControl::PreflightStatus status,
361 const ResourceResponse& response) {
362 switch (status) {
363 case kPreflightInvalidStatus: {
364 int statusCode = response.httpStatusCode();
365 builder.append("Response for preflight has invalid HTTP status code ");
366 builder.append(String::number(statusCode));
367 return;
368 }
369 case kPreflightMissingAllowExternal: {
370 builder.append(
371 "No 'Access-Control-Allow-External' header was present in ");
372 builder.append(
373 "the preflight response for this external request (This is");
374 builder.append(" an experimental header which is defined in ");
375 builder.append("'https://wicg.github.io/cors-rfc1918/').");
376 return;
377 }
378 case kPreflightInvalidAllowExternal: {
379 String result =
380 response.httpHeaderField(HTTPNames::Access_Control_Allow_External);
381 builder.append("The 'Access-Control-Allow-External' header in the ");
382 builder.append(
383 "preflight response for this external request had a value");
384 builder.append(" of '");
385 builder.append(result);
386 builder.append("', not 'true' (This is an experimental header which is");
387 builder.append(" defined in 'https://wicg.github.io/cors-rfc1918/').");
388 return;
389 }
390 default:
391 NOTREACHED();
392 }
393 }
394
395 void parseAccessControlExposeHeadersAllowList(const String& headerValue,
396 HTTPHeaderSet& headerSet) {
397 Vector<String> headers;
398 headerValue.split(',', false, headers);
399 for (unsigned headerCount = 0; headerCount < headers.size(); headerCount++) {
400 String strippedHeader = headers[headerCount].stripWhiteSpace();
401 if (!strippedHeader.isEmpty())
402 headerSet.add(strippedHeader);
403 }
404 }
405
406 void extractCorsExposedHeaderNamesList(const ResourceResponse& response,
407 HTTPHeaderSet& headerSet) {
408 // If a response was fetched via a service worker, it will always have
409 // corsExposedHeaderNames set, either from the Access-Control-Expose-Headers
410 // header, or explicitly via foreign fetch. For requests that didn't come from
411 // a service worker, foreign fetch doesn't apply so just parse the CORS
412 // header.
413 if (response.wasFetchedViaServiceWorker()) {
414 for (const auto& header : response.corsExposedHeaderNames())
415 headerSet.add(header);
416 return;
417 }
418 parseAccessControlExposeHeadersAllowList(
419 response.httpHeaderField(HTTPNames::Access_Control_Expose_Headers),
420 headerSet);
421 }
422
423 CrossOriginAccessControl::RedirectStatus
424 CrossOriginAccessControl::checkRedirectLocation(const KURL& requestURL) {
425 // Block non HTTP(S) schemes as specified in the step 4 in
426 // https://fetch.spec.whatwg.org/#http-redirect-fetch. Chromium also allows
427 // the data scheme.
428 //
429 // TODO(tyoshino): This check should be performed regardless of the CORS flag
430 // and request's mode.
431 if (!SchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(requestURL.protocol()))
432 return kRedirectDisallowedScheme;
433
434 // Block URLs including credentials as specified in the step 9 in
435 // https://fetch.spec.whatwg.org/#http-redirect-fetch.
436 //
437 // TODO(tyoshino): This check should be performed also when request's
438 // origin is not same origin with the redirect destination's origin.
439 if (!(requestURL.user().isEmpty() && requestURL.pass().isEmpty()))
440 return kRedirectContainsCredentials;
441
442 return kRedirectSuccess;
443 }
444
445 void CrossOriginAccessControl::redirectErrorString(
446 StringBuilder& builder,
447 CrossOriginAccessControl::RedirectStatus status,
448 const KURL& requestURL) {
449 switch (status) {
450 case kRedirectDisallowedScheme: {
451 builder.append("Redirect location '");
452 builder.append(requestURL.getString());
453 builder.append("' has a disallowed scheme for cross-origin requests.");
454 return;
455 }
456 case kRedirectContainsCredentials: {
457 builder.append("Redirect location '");
458 builder.append(requestURL.getString());
459 builder.append(
460 "' contains a username and password, which is disallowed for"
461 " cross-origin requests.");
462 return;
463 }
464 default:
465 NOTREACHED();
466 }
467 }
468
469 bool CrossOriginAccessControl::handleRedirect(
470 PassRefPtr<SecurityOrigin> securityOrigin,
471 ResourceRequest& newRequest,
472 const ResourceResponse& redirectResponse,
473 StoredCredentials withCredentials,
474 ResourceLoaderOptions& options,
475 String& errorMessage) {
476 // http://www.w3.org/TR/cors/#redirect-steps terminology:
477 const KURL& lastURL = redirectResponse.url();
478 const KURL& newURL = newRequest.url();
479
480 RefPtr<SecurityOrigin> currentSecurityOrigin = securityOrigin;
481
482 RefPtr<SecurityOrigin> newSecurityOrigin = currentSecurityOrigin;
483
484 // TODO(tyoshino): This should be fixed to check not only the last one but
485 // all redirect responses.
486 if (!currentSecurityOrigin->canRequest(lastURL)) {
487 // Follow http://www.w3.org/TR/cors/#redirect-steps
488 CrossOriginAccessControl::RedirectStatus redirectStatus =
489 CrossOriginAccessControl::checkRedirectLocation(newURL);
490 if (redirectStatus != kRedirectSuccess) {
491 StringBuilder builder;
492 builder.append("Redirect from '");
493 builder.append(lastURL.getString());
494 builder.append("' has been blocked by CORS policy: ");
495 CrossOriginAccessControl::redirectErrorString(builder, redirectStatus,
496 newURL);
497 errorMessage = builder.toString();
498 return false;
499 }
500
501 // Step 5: perform resource sharing access check.
502 CrossOriginAccessControl::AccessStatus corsStatus =
503 CrossOriginAccessControl::checkAccess(redirectResponse, withCredentials,
504 currentSecurityOrigin.get());
505 if (corsStatus != kAccessAllowed) {
506 StringBuilder builder;
507 builder.append("Redirect from '");
508 builder.append(lastURL.getString());
509 builder.append("' has been blocked by CORS policy: ");
510 CrossOriginAccessControl::accessControlErrorString(
511 builder, corsStatus, redirectResponse, currentSecurityOrigin.get(),
512 newRequest.requestContext());
513 errorMessage = builder.toString();
514 return false;
515 }
516
517 RefPtr<SecurityOrigin> lastOrigin = SecurityOrigin::create(lastURL);
518 // Set request's origin to a globally unique identifier as specified in
519 // the step 10 in https://fetch.spec.whatwg.org/#http-redirect-fetch.
520 if (!lastOrigin->canRequest(newURL)) {
521 options.securityOrigin = SecurityOrigin::createUnique();
522 newSecurityOrigin = options.securityOrigin;
523 }
524 }
525
526 if (!currentSecurityOrigin->canRequest(newURL)) {
527 newRequest.clearHTTPOrigin();
528 newRequest.setHTTPOrigin(newSecurityOrigin.get());
529
530 // Unset credentials flag if request's credentials mode is "same-origin" as
531 // request's response tainting becomes "cors".
532 //
533 // This is equivalent to the step 2 in
534 // https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
535 if (options.credentialsRequested == ClientDidNotRequestCredentials)
536 options.allowCredentials = DoNotAllowStoredCredentials;
537 }
538 return true;
539 }
540
541 } // namespace blink
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698