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

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: rebase 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
74 // Sort header names lexicographically.
75 std::sort(filteredHeaders.begin(), filteredHeaders.end(),
76 WTF::codePointCompareLessThan);
77 StringBuilder headerBuffer;
78 for (const String& header : filteredHeaders) {
79 if (!headerBuffer.isEmpty())
80 headerBuffer.append(",");
81 headerBuffer.append(header);
82 }
83
84 return AtomicString(headerBuffer.toString());
85 }
86
87 ResourceRequest createAccessControlPreflightRequest(
88 const ResourceRequest& request,
89 const SecurityOrigin* securityOrigin) {
90 const KURL& requestURL = request.url();
91
92 DCHECK(requestURL.user().isEmpty());
93 DCHECK(requestURL.pass().isEmpty());
94
95 ResourceRequest preflightRequest(requestURL);
96 preflightRequest.setAllowStoredCredentials(false);
97 preflightRequest.setHTTPMethod(HTTPNames::OPTIONS);
98 preflightRequest.setHTTPHeaderField(HTTPNames::Access_Control_Request_Method,
99 AtomicString(request.httpMethod()));
100 preflightRequest.setPriority(request.priority());
101 preflightRequest.setRequestContext(request.requestContext());
102 preflightRequest.setSkipServiceWorker(WebURLRequest::SkipServiceWorker::All);
103
104 if (request.isExternalRequest()) {
105 preflightRequest.setHTTPHeaderField(
106 HTTPNames::Access_Control_Request_External, "true");
107 }
108
109 if (request.httpHeaderFields().size() > 0) {
110 preflightRequest.setHTTPHeaderField(
111 HTTPNames::Access_Control_Request_Headers,
112 createAccessControlRequestHeadersHeader(request.httpHeaderFields()));
113 }
114
115 return preflightRequest;
116 }
117
118 static bool isOriginSeparator(UChar ch) {
119 return isASCIISpace(ch) || ch == ',';
120 }
121
122 static bool isInterestingStatusCode(int statusCode) {
123 // Predicate that gates what status codes should be included in console error
124 // messages for responses containing no access control headers.
125 return statusCode >= 400;
126 }
127
128 static void appendOriginDeniedMessage(StringBuilder& builder,
129 const SecurityOrigin* securityOrigin) {
130 builder.append(" Origin '");
131 builder.append(securityOrigin->toString());
132 builder.append("' is therefore not allowed access.");
133 }
134
135 static void appendNoCORSInformationalMessage(
136 StringBuilder& builder,
137 WebURLRequest::RequestContext context) {
138 if (context != WebURLRequest::RequestContextFetch)
139 return;
140 builder.append(
141 " Have the server send the header with a valid value, or, if an "
142 "opaque response serves your needs, set the request's mode to "
143 "'no-cors' to fetch the resource with CORS disabled.");
144 }
145
146 CrossOriginAccessControl::AccessStatus CrossOriginAccessControl::checkAccess(
147 const ResourceResponse& response,
148 StoredCredentials includeCredentials,
149 const SecurityOrigin* securityOrigin) {
150 DEFINE_THREAD_SAFE_STATIC_LOCAL(
151 AtomicString, allowOriginHeaderName,
152 (new AtomicString("access-control-allow-origin")));
153 DEFINE_THREAD_SAFE_STATIC_LOCAL(
154 AtomicString, allowCredentialsHeaderName,
155 (new AtomicString("access-control-allow-credentials")));
156 DEFINE_THREAD_SAFE_STATIC_LOCAL(
157 AtomicString, allowSuboriginHeaderName,
158 (new AtomicString("access-control-allow-suborigin")));
159
160 int statusCode = response.httpStatusCode();
161 if (!statusCode)
162 return kInvalidResponse;
163
164 const AtomicString& allowOriginHeaderValue =
165 response.httpHeaderField(allowOriginHeaderName);
166
167 // Check Suborigins, unless the Access-Control-Allow-Origin is '*', which
168 // implies that all Suborigins are okay as well.
169 if (securityOrigin->hasSuborigin() && allowOriginHeaderValue != starAtom) {
170 const AtomicString& allowSuboriginHeaderValue =
171 response.httpHeaderField(allowSuboriginHeaderName);
172 AtomicString atomicSuboriginName(securityOrigin->suborigin()->name());
173 if (allowSuboriginHeaderValue != starAtom &&
174 allowSuboriginHeaderValue != atomicSuboriginName) {
175 return kSubOriginMismatch;
176 }
177 }
178
179 if (allowOriginHeaderValue == starAtom) {
180 // A wildcard Access-Control-Allow-Origin can not be used if credentials are
181 // to be sent, even with Access-Control-Allow-Credentials set to true.
182 if (includeCredentials == DoNotAllowStoredCredentials)
183 return kAccessAllowed;
184 if (response.isHTTP()) {
185 return kWildcardOriginNotAllowed;
186 }
187 } else if (allowOriginHeaderValue != securityOrigin->toAtomicString()) {
188 if (allowOriginHeaderValue.isNull())
189 return kMissingAllowOriginHeader;
190 if (allowOriginHeaderValue.getString().find(isOriginSeparator, 0) !=
191 kNotFound) {
192 return kMultipleAllowOriginValues;
193 }
194 KURL headerOrigin(KURL(), allowOriginHeaderValue);
195 if (!headerOrigin.isValid())
196 return kInvalidAllowOriginValue;
197
198 return kAllowOriginMismatch;
199 }
200
201 if (includeCredentials == AllowStoredCredentials) {
202 const AtomicString& allowCredentialsHeaderValue =
203 response.httpHeaderField(allowCredentialsHeaderName);
204 if (allowCredentialsHeaderValue != "true") {
205 return kDisallowCredentialsNotSetToTrue;
206 }
207 }
208 return kAccessAllowed;
209 }
210
211 void CrossOriginAccessControl::accessControlErrorString(
212 StringBuilder& builder,
213 CrossOriginAccessControl::AccessStatus status,
214 const ResourceResponse& response,
215 const SecurityOrigin* securityOrigin,
216 WebURLRequest::RequestContext context) {
217 DEFINE_THREAD_SAFE_STATIC_LOCAL(
218 AtomicString, allowOriginHeaderName,
219 (new AtomicString("access-control-allow-origin")));
220 DEFINE_THREAD_SAFE_STATIC_LOCAL(
221 AtomicString, allowCredentialsHeaderName,
222 (new AtomicString("access-control-allow-credentials")));
223 DEFINE_THREAD_SAFE_STATIC_LOCAL(
224 AtomicString, allowSuboriginHeaderName,
225 (new AtomicString("access-control-allow-suborigin")));
226
227 switch (status) {
228 case kInvalidResponse: {
229 builder.append("Invalid response.");
230 appendOriginDeniedMessage(builder, securityOrigin);
231 return;
232 }
233 case kSubOriginMismatch: {
234 const AtomicString& allowSuboriginHeaderValue =
235 response.httpHeaderField(allowSuboriginHeaderName);
236 builder.append(
237 "The 'Access-Control-Allow-Suborigin' header has a value '");
238 builder.append(allowSuboriginHeaderValue);
239 builder.append("' that is not equal to the supplied suborigin.");
240 appendOriginDeniedMessage(builder, securityOrigin);
241 return;
242 }
243 case kWildcardOriginNotAllowed: {
244 builder.append(
245 "The value of the 'Access-Control-Allow-Origin' header in the "
246 "response must not be the wildcard '*' when the request's "
247 "credentials mode is 'include'.");
248 appendOriginDeniedMessage(builder, securityOrigin);
249 if (context == WebURLRequest::RequestContextXMLHttpRequest) {
250 builder.append(
251 " The credentials mode of requests initiated by the "
252 "XMLHttpRequest is controlled by the withCredentials attribute.");
253 }
254 return;
255 }
256 case kMissingAllowOriginHeader: {
257 builder.append(
258 "No 'Access-Control-Allow-Origin' header is present on the requested "
259 "resource.");
260 appendOriginDeniedMessage(builder, securityOrigin);
261 int statusCode = response.httpStatusCode();
262 if (isInterestingStatusCode(statusCode)) {
263 builder.append(" The response had HTTP status code ");
264 builder.append(String::number(statusCode));
265 builder.append('.');
266 }
267 if (context == WebURLRequest::RequestContextFetch) {
268 builder.append(
269 " If an opaque response serves your needs, set the request's mode "
270 "to 'no-cors' to fetch the resource with CORS disabled.");
271 }
272 return;
273 }
274 case kMultipleAllowOriginValues: {
275 const AtomicString& allowOriginHeaderValue =
276 response.httpHeaderField(allowOriginHeaderName);
277 builder.append(
278 "The 'Access-Control-Allow-Origin' header contains multiple values "
279 "'");
280 builder.append(allowOriginHeaderValue);
281 builder.append("', but only one is allowed.");
282 appendOriginDeniedMessage(builder, securityOrigin);
283 appendNoCORSInformationalMessage(builder, context);
284 return;
285 }
286 case kInvalidAllowOriginValue: {
287 const AtomicString& allowOriginHeaderValue =
288 response.httpHeaderField(allowOriginHeaderName);
289 builder.append(
290 "The 'Access-Control-Allow-Origin' header contains the invalid "
291 "value '");
292 builder.append(allowOriginHeaderValue);
293 builder.append("'.");
294 appendOriginDeniedMessage(builder, securityOrigin);
295 appendNoCORSInformationalMessage(builder, context);
296 return;
297 }
298 case kAllowOriginMismatch: {
299 const AtomicString& allowOriginHeaderValue =
300 response.httpHeaderField(allowOriginHeaderName);
301 builder.append("The 'Access-Control-Allow-Origin' header has a value '");
302 builder.append(allowOriginHeaderValue);
303 builder.append("' that is not equal to the supplied origin.");
304 appendOriginDeniedMessage(builder, securityOrigin);
305 appendNoCORSInformationalMessage(builder, context);
306 return;
307 }
308 case kDisallowCredentialsNotSetToTrue: {
309 const AtomicString& allowCredentialsHeaderValue =
310 response.httpHeaderField(allowCredentialsHeaderName);
311 builder.append(
312 "The value of the 'Access-Control-Allow-Credentials' header in "
313 "the response is '");
314 builder.append(allowCredentialsHeaderValue);
315 builder.append(
316 "' which must "
317 "be 'true' when the request's credentials mode is 'include'.");
318 appendOriginDeniedMessage(builder, securityOrigin);
319 if (context == WebURLRequest::RequestContextXMLHttpRequest) {
320 builder.append(
321 " The credentials mode of requests initiated by the "
322 "XMLHttpRequest is controlled by the withCredentials attribute.");
323 }
324 return;
325 }
326 default:
327 NOTREACHED();
328 }
329 }
330
331 CrossOriginAccessControl::PreflightStatus
332 CrossOriginAccessControl::checkPreflight(const ResourceResponse& response) {
333 // CORS preflight with 3XX is considered network error in
334 // Fetch API Spec: https://fetch.spec.whatwg.org/#cors-preflight-fetch
335 // CORS Spec: http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0
336 // https://crbug.com/452394
337 int statusCode = response.httpStatusCode();
338 if (!FetchUtils::isOkStatus(statusCode))
339 return kPreflightInvalidStatus;
340
341 return kPreflightSuccess;
342 }
343
344 CrossOriginAccessControl::PreflightStatus
345 CrossOriginAccessControl::checkExternalPreflight(
346 const ResourceResponse& response) {
347 AtomicString result =
348 response.httpHeaderField(HTTPNames::Access_Control_Allow_External);
349 if (result.isNull())
350 return kPreflightMissingAllowExternal;
351 if (!equalIgnoringCase(result, "true"))
352 return kPreflightInvalidAllowExternal;
353 return kPreflightSuccess;
354 }
355
356 void CrossOriginAccessControl::preflightErrorString(
357 StringBuilder& builder,
358 CrossOriginAccessControl::PreflightStatus status,
359 const ResourceResponse& response) {
360 switch (status) {
361 case kPreflightInvalidStatus: {
362 int statusCode = response.httpStatusCode();
363 builder.append("Response for preflight has invalid HTTP status code ");
364 builder.append(String::number(statusCode));
365 return;
366 }
367 case kPreflightMissingAllowExternal: {
368 builder.append(
369 "No 'Access-Control-Allow-External' header was present in ");
370 builder.append(
371 "the preflight response for this external request (This is");
372 builder.append(" an experimental header which is defined in ");
373 builder.append("'https://wicg.github.io/cors-rfc1918/').");
374 return;
375 }
376 case kPreflightInvalidAllowExternal: {
377 String result =
378 response.httpHeaderField(HTTPNames::Access_Control_Allow_External);
379 builder.append("The 'Access-Control-Allow-External' header in the ");
380 builder.append(
381 "preflight response for this external request had a value");
382 builder.append(" of '");
383 builder.append(result);
384 builder.append("', not 'true' (This is an experimental header which is");
385 builder.append(" defined in 'https://wicg.github.io/cors-rfc1918/').");
386 return;
387 }
388 default:
389 NOTREACHED();
390 }
391 }
392
393 void parseAccessControlExposeHeadersAllowList(const String& headerValue,
394 HTTPHeaderSet& headerSet) {
395 Vector<String> headers;
396 headerValue.split(',', false, headers);
397 for (unsigned headerCount = 0; headerCount < headers.size(); headerCount++) {
398 String strippedHeader = headers[headerCount].stripWhiteSpace();
399 if (!strippedHeader.isEmpty())
400 headerSet.add(strippedHeader);
401 }
402 }
403
404 void extractCorsExposedHeaderNamesList(const ResourceResponse& response,
405 HTTPHeaderSet& headerSet) {
406 // If a response was fetched via a service worker, it will always have
407 // corsExposedHeaderNames set, either from the Access-Control-Expose-Headers
408 // header, or explicitly via foreign fetch. For requests that didn't come from
409 // a service worker, foreign fetch doesn't apply so just parse the CORS
410 // header.
411 if (response.wasFetchedViaServiceWorker()) {
412 for (const auto& header : response.corsExposedHeaderNames())
413 headerSet.add(header);
414 return;
415 }
416 parseAccessControlExposeHeadersAllowList(
417 response.httpHeaderField(HTTPNames::Access_Control_Expose_Headers),
418 headerSet);
419 }
420
421 CrossOriginAccessControl::RedirectStatus
422 CrossOriginAccessControl::checkRedirectLocation(const KURL& requestURL) {
423 // Block non HTTP(S) schemes as specified in the step 4 in
424 // https://fetch.spec.whatwg.org/#http-redirect-fetch. Chromium also allows
425 // the data scheme.
426 //
427 // TODO(tyoshino): This check should be performed regardless of the CORS flag
428 // and request's mode.
429 if (!SchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(requestURL.protocol()))
430 return kRedirectDisallowedScheme;
431
432 // Block URLs including credentials as specified in the step 9 in
433 // https://fetch.spec.whatwg.org/#http-redirect-fetch.
434 //
435 // TODO(tyoshino): This check should be performed also when request's
436 // origin is not same origin with the redirect destination's origin.
437 if (!(requestURL.user().isEmpty() && requestURL.pass().isEmpty()))
438 return kRedirectContainsCredentials;
439
440 return kRedirectSuccess;
441 }
442
443 void CrossOriginAccessControl::redirectErrorString(
444 StringBuilder& builder,
445 CrossOriginAccessControl::RedirectStatus status,
446 const KURL& requestURL) {
447 switch (status) {
448 case kRedirectDisallowedScheme: {
449 builder.append("Redirect location '");
450 builder.append(requestURL.getString());
451 builder.append("' has a disallowed scheme for cross-origin requests.");
452 return;
453 }
454 case kRedirectContainsCredentials: {
455 builder.append("Redirect location '");
456 builder.append(requestURL.getString());
457 builder.append(
458 "' contains a username and password, which is disallowed for"
459 " cross-origin requests.");
460 return;
461 }
462 default:
463 NOTREACHED();
464 }
465 }
466
467 bool CrossOriginAccessControl::handleRedirect(
468 PassRefPtr<SecurityOrigin> securityOrigin,
469 ResourceRequest& newRequest,
470 const ResourceResponse& redirectResponse,
471 StoredCredentials withCredentials,
472 ResourceLoaderOptions& options,
473 String& errorMessage) {
474 // http://www.w3.org/TR/cors/#redirect-steps terminology:
475 const KURL& lastURL = redirectResponse.url();
476 const KURL& newURL = newRequest.url();
477
478 RefPtr<SecurityOrigin> currentSecurityOrigin = securityOrigin;
479
480 RefPtr<SecurityOrigin> newSecurityOrigin = currentSecurityOrigin;
481
482 // TODO(tyoshino): This should be fixed to check not only the last one but
483 // all redirect responses.
484 if (!currentSecurityOrigin->canRequest(lastURL)) {
485 // Follow http://www.w3.org/TR/cors/#redirect-steps
486 CrossOriginAccessControl::RedirectStatus redirectStatus =
487 CrossOriginAccessControl::checkRedirectLocation(newURL);
488 if (redirectStatus != kRedirectSuccess) {
489 StringBuilder builder;
490 builder.append("Redirect from '");
491 builder.append(lastURL.getString());
492 builder.append("' has been blocked by CORS policy: ");
493 CrossOriginAccessControl::redirectErrorString(builder, redirectStatus,
494 newURL);
495 errorMessage = builder.toString();
496 return false;
497 }
498
499 // Step 5: perform resource sharing access check.
500 CrossOriginAccessControl::AccessStatus corsStatus =
501 CrossOriginAccessControl::checkAccess(redirectResponse, withCredentials,
502 currentSecurityOrigin.get());
503 if (corsStatus != kAccessAllowed) {
504 StringBuilder builder;
505 builder.append("Redirect from '");
506 builder.append(lastURL.getString());
507 builder.append("' has been blocked by CORS policy: ");
508 CrossOriginAccessControl::accessControlErrorString(
509 builder, corsStatus, redirectResponse, currentSecurityOrigin.get(),
510 newRequest.requestContext());
511 errorMessage = builder.toString();
512 return false;
513 }
514
515 RefPtr<SecurityOrigin> lastOrigin = SecurityOrigin::create(lastURL);
516 // Set request's origin to a globally unique identifier as specified in
517 // the step 10 in https://fetch.spec.whatwg.org/#http-redirect-fetch.
518 if (!lastOrigin->canRequest(newURL)) {
519 options.securityOrigin = SecurityOrigin::createUnique();
520 newSecurityOrigin = options.securityOrigin;
521 }
522 }
523
524 if (!currentSecurityOrigin->canRequest(newURL)) {
525 newRequest.clearHTTPOrigin();
526 newRequest.setHTTPOrigin(newSecurityOrigin.get());
527
528 // Unset credentials flag if request's credentials mode is "same-origin" as
529 // request's response tainting becomes "cors".
530 //
531 // This is equivalent to the step 2 in
532 // https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
533 if (options.credentialsRequested == ClientDidNotRequestCredentials)
534 options.allowCredentials = DoNotAllowStoredCredentials;
535 }
536 return true;
537 }
538
539 } // namespace blink
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698