Index: third_party/WebKit/Source/core/fetch/CrossOriginAccessControl.cpp |
diff --git a/third_party/WebKit/Source/core/fetch/CrossOriginAccessControl.cpp b/third_party/WebKit/Source/core/fetch/CrossOriginAccessControl.cpp |
index 5385c8d885640dbbad7760972069eb3450f7cb09..19352dabe85a225136cb8b60f73bf6c632a2244b 100644 |
--- a/third_party/WebKit/Source/core/fetch/CrossOriginAccessControl.cpp |
+++ b/third_party/WebKit/Source/core/fetch/CrossOriginAccessControl.cpp |
@@ -125,18 +125,28 @@ static bool isInterestingStatusCode(int statusCode) { |
return statusCode >= 400; |
} |
-static String buildAccessControlFailureMessage( |
- const String& detail, |
- const SecurityOrigin* securityOrigin) { |
- return detail + " Origin '" + securityOrigin->toString() + |
- "' is therefore not allowed access."; |
+static void appendOriginDeniedMessage(StringBuilder& builder, |
+ const SecurityOrigin* securityOrigin) { |
+ builder.append(" Origin '"); |
+ builder.append(securityOrigin->toString()); |
+ builder.append("' is therefore not allowed access."); |
} |
-bool passesAccessControlCheck(const ResourceResponse& response, |
- StoredCredentials includeCredentials, |
- const SecurityOrigin* securityOrigin, |
- String& errorDescription, |
- WebURLRequest::RequestContext context) { |
+static void appendNoCORSInformationalMessage( |
+ StringBuilder& builder, |
+ WebURLRequest::RequestContext context) { |
+ if (context != WebURLRequest::RequestContextFetch) |
+ return; |
+ builder.append( |
+ " Have the server send the header with a valid value, or, if an " |
+ "opaque response serves your needs, set the request's mode to " |
+ "'no-cors' to fetch the resource with CORS disabled."); |
+} |
+ |
+CrossOriginAccessControl::AccessStatus CrossOriginAccessControl::checkAccess( |
+ const ResourceResponse& response, |
+ StoredCredentials includeCredentials, |
+ const SecurityOrigin* securityOrigin) { |
DEFINE_THREAD_SAFE_STATIC_LOCAL( |
AtomicString, allowOriginHeaderName, |
(new AtomicString("access-control-allow-origin"))); |
@@ -147,16 +157,9 @@ bool passesAccessControlCheck(const ResourceResponse& response, |
AtomicString, allowSuboriginHeaderName, |
(new AtomicString("access-control-allow-suborigin"))); |
- // TODO(esprehn): This code is using String::append extremely inefficiently |
- // causing tons of copies. It should pass around a StringBuilder instead. |
- |
int statusCode = response.httpStatusCode(); |
- |
- if (!statusCode) { |
- errorDescription = |
- buildAccessControlFailureMessage("Invalid response.", securityOrigin); |
- return false; |
- } |
+ if (!statusCode) |
+ return kInvalidResponse; |
const AtomicString& allowOriginHeaderValue = |
response.httpHeaderField(allowOriginHeaderName); |
@@ -169,12 +172,7 @@ bool passesAccessControlCheck(const ResourceResponse& response, |
AtomicString atomicSuboriginName(securityOrigin->suborigin()->name()); |
if (allowSuboriginHeaderValue != starAtom && |
allowSuboriginHeaderValue != atomicSuboriginName) { |
- errorDescription = buildAccessControlFailureMessage( |
- "The 'Access-Control-Allow-Suborigin' header has a value '" + |
- allowSuboriginHeaderValue + |
- "' that is not equal to the supplied suborigin.", |
- securityOrigin); |
- return false; |
+ return kSubOriginMismatch; |
} |
} |
@@ -182,137 +180,214 @@ bool passesAccessControlCheck(const ResourceResponse& response, |
// A wildcard Access-Control-Allow-Origin can not be used if credentials are |
// to be sent, even with Access-Control-Allow-Credentials set to true. |
if (includeCredentials == DoNotAllowStoredCredentials) |
- return true; |
+ return kAccessAllowed; |
if (response.isHTTP()) { |
- errorDescription = buildAccessControlFailureMessage( |
+ return kWildcardOriginNotAllowed; |
+ } |
+ } else if (allowOriginHeaderValue != securityOrigin->toAtomicString()) { |
+ if (allowOriginHeaderValue.isNull()) |
+ return kMissingAllowOriginHeader; |
+ if (allowOriginHeaderValue.getString().find(isOriginSeparator, 0) != |
+ kNotFound) { |
+ return kMultipleAllowOriginValues; |
+ } |
+ KURL headerOrigin(KURL(), allowOriginHeaderValue); |
+ if (!headerOrigin.isValid()) |
+ return kInvalidAllowOriginValue; |
+ |
+ return kAllowOriginMismatch; |
+ } |
+ |
+ if (includeCredentials == AllowStoredCredentials) { |
+ const AtomicString& allowCredentialsHeaderValue = |
+ response.httpHeaderField(allowCredentialsHeaderName); |
+ if (allowCredentialsHeaderValue != "true") { |
+ return kDisallowCredentialsNotSetToTrue; |
+ } |
+ } |
+ return kAccessAllowed; |
+} |
+ |
+void CrossOriginAccessControl::accessControlErrorString( |
+ StringBuilder& builder, |
+ CrossOriginAccessControl::AccessStatus status, |
+ const ResourceResponse& response, |
+ const SecurityOrigin* securityOrigin, |
+ WebURLRequest::RequestContext context) { |
+ DEFINE_THREAD_SAFE_STATIC_LOCAL( |
+ AtomicString, allowOriginHeaderName, |
+ (new AtomicString("access-control-allow-origin"))); |
+ DEFINE_THREAD_SAFE_STATIC_LOCAL( |
+ AtomicString, allowCredentialsHeaderName, |
+ (new AtomicString("access-control-allow-credentials"))); |
+ DEFINE_THREAD_SAFE_STATIC_LOCAL( |
+ AtomicString, allowSuboriginHeaderName, |
+ (new AtomicString("access-control-allow-suborigin"))); |
+ |
+ switch (status) { |
+ case kInvalidResponse: { |
+ builder.append("Invalid response."); |
+ appendOriginDeniedMessage(builder, securityOrigin); |
+ return; |
+ } |
+ case kSubOriginMismatch: { |
+ const AtomicString& allowSuboriginHeaderValue = |
+ response.httpHeaderField(allowSuboriginHeaderName); |
+ builder.append( |
+ "The 'Access-Control-Allow-Suborigin' header has a value '"); |
+ builder.append(allowSuboriginHeaderValue); |
+ builder.append("' that is not equal to the supplied suborigin."); |
+ appendOriginDeniedMessage(builder, securityOrigin); |
+ return; |
+ } |
+ case kWildcardOriginNotAllowed: { |
+ builder.append( |
"The value of the 'Access-Control-Allow-Origin' header in the " |
"response must not be the wildcard '*' when the request's " |
- "credentials mode is 'include'.", |
- securityOrigin); |
- |
+ "credentials mode is 'include'."); |
+ appendOriginDeniedMessage(builder, securityOrigin); |
if (context == WebURLRequest::RequestContextXMLHttpRequest) { |
- errorDescription.append( |
+ builder.append( |
" The credentials mode of requests initiated by the " |
"XMLHttpRequest is controlled by the withCredentials attribute."); |
} |
- |
- return false; |
+ return; |
} |
- } else if (allowOriginHeaderValue != securityOrigin->toAtomicString()) { |
- if (allowOriginHeaderValue.isNull()) { |
- errorDescription = buildAccessControlFailureMessage( |
+ case kMissingAllowOriginHeader: { |
+ builder.append( |
"No 'Access-Control-Allow-Origin' header is present on the requested " |
- "resource.", |
- securityOrigin); |
- |
+ "resource."); |
+ appendOriginDeniedMessage(builder, securityOrigin); |
+ int statusCode = response.httpStatusCode(); |
if (isInterestingStatusCode(statusCode)) { |
- errorDescription.append(" The response had HTTP status code "); |
- errorDescription.append(String::number(statusCode)); |
- errorDescription.append('.'); |
+ builder.append(" The response had HTTP status code "); |
+ builder.append(String::number(statusCode)); |
+ builder.append('.'); |
} |
- |
if (context == WebURLRequest::RequestContextFetch) { |
- errorDescription.append( |
+ builder.append( |
" If an opaque response serves your needs, set the request's mode " |
"to 'no-cors' to fetch the resource with CORS disabled."); |
} |
- |
- return false; |
+ return; |
} |
- |
- String detail; |
- if (allowOriginHeaderValue.getString().find(isOriginSeparator, 0) != |
- kNotFound) { |
- detail = |
+ case kMultipleAllowOriginValues: { |
+ const AtomicString& allowOriginHeaderValue = |
+ response.httpHeaderField(allowOriginHeaderName); |
+ builder.append( |
"The 'Access-Control-Allow-Origin' header contains multiple values " |
- "'" + |
- allowOriginHeaderValue + "', but only one is allowed."; |
- } else { |
- KURL headerOrigin(KURL(), allowOriginHeaderValue); |
- if (!headerOrigin.isValid()) { |
- detail = |
- "The 'Access-Control-Allow-Origin' header contains the invalid " |
- "value '" + |
- allowOriginHeaderValue + "'."; |
- } else { |
- detail = "The 'Access-Control-Allow-Origin' header has a value '" + |
- allowOriginHeaderValue + |
- "' that is not equal to the supplied origin."; |
- } |
+ "'"); |
+ builder.append(allowOriginHeaderValue); |
+ builder.append("', but only one is allowed."); |
+ appendOriginDeniedMessage(builder, securityOrigin); |
+ appendNoCORSInformationalMessage(builder, context); |
+ return; |
} |
- errorDescription = buildAccessControlFailureMessage(detail, securityOrigin); |
- if (context == WebURLRequest::RequestContextFetch) { |
- errorDescription.append( |
- " Have the server send the header with a valid value, or, if an " |
- "opaque response serves your needs, set the request's mode to " |
- "'no-cors' to fetch the resource with CORS disabled."); |
+ case kInvalidAllowOriginValue: { |
+ const AtomicString& allowOriginHeaderValue = |
+ response.httpHeaderField(allowOriginHeaderName); |
+ builder.append( |
+ "The 'Access-Control-Allow-Origin' header contains the invalid " |
+ "value '"); |
+ builder.append(allowOriginHeaderValue); |
+ builder.append("'."); |
+ appendOriginDeniedMessage(builder, securityOrigin); |
+ appendNoCORSInformationalMessage(builder, context); |
+ return; |
} |
- return false; |
- } |
- |
- if (includeCredentials == AllowStoredCredentials) { |
- const AtomicString& allowCredentialsHeaderValue = |
- response.httpHeaderField(allowCredentialsHeaderName); |
- if (allowCredentialsHeaderValue != "true") { |
- errorDescription = buildAccessControlFailureMessage( |
+ case kAllowOriginMismatch: { |
+ const AtomicString& allowOriginHeaderValue = |
+ response.httpHeaderField(allowOriginHeaderName); |
+ builder.append("The 'Access-Control-Allow-Origin' header has a value '"); |
+ builder.append(allowOriginHeaderValue); |
+ builder.append("' that is not equal to the supplied origin."); |
+ appendOriginDeniedMessage(builder, securityOrigin); |
+ appendNoCORSInformationalMessage(builder, context); |
+ return; |
+ } |
+ case kDisallowCredentialsNotSetToTrue: { |
+ const AtomicString& allowCredentialsHeaderValue = |
+ response.httpHeaderField(allowCredentialsHeaderName); |
+ builder.append( |
"The value of the 'Access-Control-Allow-Credentials' header in " |
- "the response is '" + |
- allowCredentialsHeaderValue + |
- "' which must " |
- "be 'true' when the request's credentials mode is 'include'.", |
- securityOrigin); |
- |
+ "the response is '"); |
+ builder.append(allowCredentialsHeaderValue); |
+ builder.append( |
+ "' which must " |
+ "be 'true' when the request's credentials mode is 'include'."); |
+ appendOriginDeniedMessage(builder, securityOrigin); |
if (context == WebURLRequest::RequestContextXMLHttpRequest) { |
- errorDescription.append( |
+ builder.append( |
" The credentials mode of requests initiated by the " |
"XMLHttpRequest is controlled by the withCredentials attribute."); |
} |
- |
- return false; |
+ return; |
} |
+ default: |
+ NOTREACHED(); |
} |
- |
- return true; |
} |
-bool passesPreflightStatusCheck(const ResourceResponse& response, |
- String& errorDescription) { |
+CrossOriginAccessControl::PreflightStatus |
+CrossOriginAccessControl::checkPreflight(const ResourceResponse& response) { |
// CORS preflight with 3XX is considered network error in |
// Fetch API Spec: https://fetch.spec.whatwg.org/#cors-preflight-fetch |
// CORS Spec: http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0 |
// https://crbug.com/452394 |
int statusCode = response.httpStatusCode(); |
- if (!FetchUtils::isOkStatus(statusCode)) { |
- errorDescription = "Response for preflight has invalid HTTP status code " + |
- String::number(statusCode); |
- return false; |
- } |
+ if (!FetchUtils::isOkStatus(statusCode)) |
+ return kPreflightInvalidStatus; |
- return true; |
+ return kPreflightSuccess; |
} |
-bool passesExternalPreflightCheck(const ResourceResponse& response, |
- String& errorDescription) { |
+CrossOriginAccessControl::PreflightStatus |
+CrossOriginAccessControl::checkExternalPreflight( |
+ const ResourceResponse& response) { |
AtomicString result = |
response.httpHeaderField(HTTPNames::Access_Control_Allow_External); |
- if (result.isNull()) { |
- errorDescription = |
- "No 'Access-Control-Allow-External' header was present in the " |
- "preflight response for this external request (This is an experimental " |
- "header which is defined in " |
- "'https://mikewest.github.io/cors-rfc1918/')."; |
- return false; |
- } |
- if (!equalIgnoringCase(result, "true")) { |
- errorDescription = |
- "The 'Access-Control-Allow-External' header in the preflight response " |
- "for this external request had a value of '" + |
- result + |
- "', not 'true' (This is an experimental header which is defined in " |
- "'https://mikewest.github.io/cors-rfc1918/')."; |
- return false; |
+ if (result.isNull()) |
+ return kPreflightMissingAllowExternal; |
+ if (!equalIgnoringCase(result, "true")) |
+ return kPreflightInvalidAllowExternal; |
+ return kPreflightSuccess; |
+} |
+ |
+void CrossOriginAccessControl::preflightErrorString( |
+ StringBuilder& builder, |
+ CrossOriginAccessControl::PreflightStatus status, |
+ const ResourceResponse& response) { |
+ switch (status) { |
+ case kPreflightInvalidStatus: { |
+ int statusCode = response.httpStatusCode(); |
+ builder.append("Response for preflight has invalid HTTP status code "); |
+ builder.append(String::number(statusCode)); |
+ return; |
+ } |
+ case kPreflightMissingAllowExternal: { |
+ builder.append( |
+ "No 'Access-Control-Allow-External' header was present in "); |
+ builder.append( |
+ "the preflight response for this external request (This is"); |
+ builder.append(" an experimental header which is defined in "); |
+ builder.append("'https://wicg.github.io/cors-rfc1918/')."); |
+ return; |
+ } |
+ case kPreflightInvalidAllowExternal: { |
+ String result = |
+ response.httpHeaderField(HTTPNames::Access_Control_Allow_External); |
+ builder.append("The 'Access-Control-Allow-External' header in the "); |
+ builder.append( |
+ "preflight response for this external request had a value"); |
+ builder.append(" of '"); |
+ builder.append(result); |
+ builder.append("', not 'true' (This is an experimental header which is"); |
+ builder.append(" defined in 'https://wicg.github.io/cors-rfc1918/')."); |
+ return; |
+ } |
+ default: |
+ NOTREACHED(); |
} |
- return true; |
} |
void parseAccessControlExposeHeadersAllowList(const String& headerValue, |
@@ -343,35 +418,50 @@ void extractCorsExposedHeaderNamesList(const ResourceResponse& response, |
headerSet); |
} |
-bool CrossOriginAccessControl::isLegalRedirectLocation( |
- const KURL& requestURL, |
- String& errorDescription) { |
+CrossOriginAccessControl::RedirectStatus |
+CrossOriginAccessControl::checkRedirectLocation(const KURL& requestURL) { |
// Block non HTTP(S) schemes as specified in the step 4 in |
// https://fetch.spec.whatwg.org/#http-redirect-fetch. Chromium also allows |
// the data scheme. |
// |
// TODO(tyoshino): This check should be performed regardless of the CORS flag |
// and request's mode. |
- if (!SchemeRegistry::shouldTreatURLSchemeAsCORSEnabled( |
- requestURL.protocol())) { |
- errorDescription = "Redirect location '" + requestURL.getString() + |
- "' has a disallowed scheme for cross-origin requests."; |
- return false; |
- } |
+ if (!SchemeRegistry::shouldTreatURLSchemeAsCORSEnabled(requestURL.protocol())) |
+ return kRedirectDisallowedScheme; |
// Block URLs including credentials as specified in the step 9 in |
// https://fetch.spec.whatwg.org/#http-redirect-fetch. |
// |
// TODO(tyoshino): This check should be performed also when request's |
// origin is not same origin with the redirect destination's origin. |
- if (!(requestURL.user().isEmpty() && requestURL.pass().isEmpty())) { |
- errorDescription = |
- "Redirect location '" + requestURL.getString() + |
- "' contains userinfo, which is disallowed for cross-origin requests."; |
- return false; |
- } |
+ if (!(requestURL.user().isEmpty() && requestURL.pass().isEmpty())) |
+ return kRedirectContainsCredentials; |
- return true; |
+ return kRedirectSuccess; |
+} |
+ |
+void CrossOriginAccessControl::redirectErrorString( |
+ StringBuilder& builder, |
+ CrossOriginAccessControl::RedirectStatus status, |
+ const KURL& requestURL) { |
+ switch (status) { |
+ case kRedirectDisallowedScheme: { |
+ builder.append("Redirect location '"); |
+ builder.append(requestURL.getString()); |
+ builder.append("' has a disallowed scheme for cross-origin requests."); |
+ return; |
+ } |
+ case kRedirectContainsCredentials: { |
+ builder.append("Redirect location '"); |
+ builder.append(requestURL.getString()); |
+ builder.append( |
+ "' contains a username and password, which is disallowed for" |
+ " cross-origin requests."); |
+ return; |
+ } |
+ default: |
+ NOTREACHED(); |
+ } |
} |
bool CrossOriginAccessControl::handleRedirect( |
@@ -393,20 +483,32 @@ bool CrossOriginAccessControl::handleRedirect( |
// all redirect responses. |
if (!currentSecurityOrigin->canRequest(lastURL)) { |
// Follow http://www.w3.org/TR/cors/#redirect-steps |
- String errorDescription; |
- |
- if (!isLegalRedirectLocation(newURL, errorDescription)) { |
- errorMessage = "Redirect from '" + lastURL.getString() + |
- "' has been blocked by CORS policy: " + errorDescription; |
+ CrossOriginAccessControl::RedirectStatus redirectStatus = |
+ CrossOriginAccessControl::checkRedirectLocation(newURL); |
+ if (redirectStatus != kRedirectSuccess) { |
+ StringBuilder builder; |
+ builder.append("Redirect from '"); |
+ builder.append(lastURL.getString()); |
+ builder.append("' has been blocked by CORS policy: "); |
+ CrossOriginAccessControl::redirectErrorString(builder, redirectStatus, |
+ newURL); |
+ errorMessage = builder.toString(); |
return false; |
} |
// Step 5: perform resource sharing access check. |
- if (!passesAccessControlCheck(redirectResponse, withCredentials, |
- currentSecurityOrigin.get(), errorDescription, |
- newRequest.requestContext())) { |
- errorMessage = "Redirect from '" + lastURL.getString() + |
- "' has been blocked by CORS policy: " + errorDescription; |
+ CrossOriginAccessControl::AccessStatus corsStatus = |
+ CrossOriginAccessControl::checkAccess(redirectResponse, withCredentials, |
+ currentSecurityOrigin.get()); |
+ if (corsStatus != kAccessAllowed) { |
+ StringBuilder builder; |
+ builder.append("Redirect from '"); |
+ builder.append(lastURL.getString()); |
+ builder.append("' has been blocked by CORS policy: "); |
+ CrossOriginAccessControl::accessControlErrorString( |
+ builder, corsStatus, redirectResponse, currentSecurityOrigin.get(), |
+ newRequest.requestContext()); |
+ errorMessage = builder.toString(); |
return false; |
} |