Bug 608735. Fix getAllResponseHeaders for cross-origin requests to actually expose the headers CORS says it can expose. r=bzbarsky

This commit is contained in:
florin.botis@gmail.com 2013-01-10 17:47:43 -05:00
parent 382d12416e
commit dffc5ee5ab
5 changed files with 118 additions and 80 deletions

View File

@ -1336,6 +1336,64 @@ nsXMLHttpRequest::SlowAbort()
return NS_OK;
}
/*Method that checks if it is safe to expose a header value to the client.
It is used to check what headers are exposed for CORS requests.*/
bool
nsXMLHttpRequest::IsSafeHeader(const nsACString& header, nsIHttpChannel* httpChannel)
{
// See bug #380418. Hide "Set-Cookie" headers from non-chrome scripts.
if (!nsContentUtils::IsCallerChrome() &&
(header.LowerCaseEqualsASCII("set-cookie") ||
header.LowerCaseEqualsASCII("set-cookie2"))) {
NS_WARNING("blocked access to response header");
return false;
}
// if this is not a CORS call all headers are safe
if (!(mState & XML_HTTP_REQUEST_USE_XSITE_AC)){
return true;
}
// Check for dangerous headers
// Make sure we don't leak header information from denied cross-site
// requests.
if (mChannel) {
nsresult status;
mChannel->GetStatus(&status);
if (NS_FAILED(status)) {
return false;
}
}
const char* kCrossOriginSafeHeaders[] = {
"cache-control", "content-language", "content-type", "expires",
"last-modified", "pragma"
};
for (uint32_t i = 0; i < ArrayLength(kCrossOriginSafeHeaders); ++i) {
if (header.LowerCaseEqualsASCII(kCrossOriginSafeHeaders[i])) {
return true;
}
}
nsAutoCString headerVal;
// The "Access-Control-Expose-Headers" header contains a comma separated
// list of method names.
httpChannel->
GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Expose-Headers"),
headerVal);
nsCCharSeparatedTokenizer exposeTokens(headerVal, ',');
bool isSafe = false;
while (exposeTokens.hasMoreTokens()) {
const nsDependentCSubstring& token = exposeTokens.nextToken();
if (token.IsEmpty()) {
continue;
}
if (!IsValidHTTPToken(token)) {
return false;
}
if (header.Equals(token, nsCaseInsensitiveCStringComparator())) {
isSafe = true;
}
}
return isSafe;
}
/* DOMString getAllResponseHeaders(); */
IMPL_STRING_GETTER(GetAllResponseHeaders)
void
@ -1350,12 +1408,8 @@ nsXMLHttpRequest::GetAllResponseHeaders(nsString& aResponseHeaders)
return;
}
if (mState & XML_HTTP_REQUEST_USE_XSITE_AC) {
return;
}
if (nsCOMPtr<nsIHttpChannel> httpChannel = GetCurrentHttpChannel()) {
nsRefPtr<nsHeaderVisitor> visitor = new nsHeaderVisitor();
nsRefPtr<nsHeaderVisitor> visitor = new nsHeaderVisitor(this, httpChannel);
if (NS_SUCCEEDED(httpChannel->VisitResponseHeaders(visitor))) {
CopyASCIItoUTF16(visitor->Headers(), aResponseHeaders);
}
@ -1448,64 +1502,9 @@ nsXMLHttpRequest::GetResponseHeader(const nsACString& header,
return;
}
// See bug #380418. Hide "Set-Cookie" headers from non-chrome scripts.
if (!nsContentUtils::IsCallerChrome() &&
(header.LowerCaseEqualsASCII("set-cookie") ||
header.LowerCaseEqualsASCII("set-cookie2"))) {
NS_WARNING("blocked access to response header");
return;
}
// Check for dangerous headers
if (mState & XML_HTTP_REQUEST_USE_XSITE_AC) {
// Make sure we don't leak header information from denied cross-site
// requests.
if (mChannel) {
nsresult status;
mChannel->GetStatus(&status);
if (NS_FAILED(status)) {
return;
}
}
const char *kCrossOriginSafeHeaders[] = {
"cache-control", "content-language", "content-type", "expires",
"last-modified", "pragma"
};
bool safeHeader = false;
uint32_t i;
for (i = 0; i < ArrayLength(kCrossOriginSafeHeaders); ++i) {
if (header.LowerCaseEqualsASCII(kCrossOriginSafeHeaders[i])) {
safeHeader = true;
break;
}
}
if (!safeHeader) {
nsAutoCString headerVal;
// The "Access-Control-Expose-Headers" header contains a comma separated
// list of method names.
httpChannel->
GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Expose-Headers"),
headerVal);
nsCCharSeparatedTokenizer exposeTokens(headerVal, ',');
while(exposeTokens.hasMoreTokens()) {
const nsDependentCSubstring& token = exposeTokens.nextToken();
if (token.IsEmpty()) {
continue;
}
if (!IsValidHTTPToken(token)) {
return;
}
if (header.Equals(token, nsCaseInsensitiveCStringComparator())) {
safeHeader = true;
}
}
}
if (!safeHeader) {
return;
}
if (!IsSafeHeader(header, httpChannel)) {
return;
}
aRv = httpChannel->GetResponseHeader(header, _retval);
@ -3989,18 +3988,13 @@ NS_IMPL_ISUPPORTS1(nsXMLHttpRequest::nsHeaderVisitor, nsIHttpHeaderVisitor)
NS_IMETHODIMP nsXMLHttpRequest::
nsHeaderVisitor::VisitHeader(const nsACString &header, const nsACString &value)
{
// See bug #380418. Hide "Set-Cookie" headers from non-chrome scripts.
if (!nsContentUtils::IsCallerChrome() &&
(header.LowerCaseEqualsASCII("set-cookie") ||
header.LowerCaseEqualsASCII("set-cookie2"))) {
NS_WARNING("blocked access to response header");
} else {
mHeaders.Append(header);
mHeaders.Append(": ");
mHeaders.Append(value);
mHeaders.Append("\r\n");
}
return NS_OK;
if (mXHR->IsSafeHeader(header, mHttpChannel)) {
mHeaders.Append(header);
mHeaders.Append(": ");
mHeaders.Append(value);
mHeaders.Append("\r\n");
}
return NS_OK;
}
// DOM event class to handle progress notifications

View File

@ -417,6 +417,7 @@ public:
}
}
void GetAllResponseHeaders(nsString& aResponseHeaders);
bool IsSafeHeader(const nsACString& aHeaderName, nsIHttpChannel* aHttpChannel);
void OverrideMimeType(const nsAString& aMimeType)
{
// XXX Should we do some validation here?
@ -552,11 +553,14 @@ protected:
public:
NS_DECL_ISUPPORTS
NS_DECL_NSIHTTPHEADERVISITOR
nsHeaderVisitor() { }
nsHeaderVisitor(nsXMLHttpRequest* aXMLHttpRequest, nsIHttpChannel* aHttpChannel)
: mXHR(aXMLHttpRequest), mHttpChannel(aHttpChannel) {}
virtual ~nsHeaderVisitor() {}
const nsACString &Headers() { return mHeaders; }
private:
nsCString mHeaders;
nsXMLHttpRequest* mXHR;
nsCOMPtr<nsIHttpChannel> mHttpChannel;
};
// The bytes of our response body. Only used for DEFAULT, ARRAYBUFFER and

View File

@ -8,6 +8,10 @@
<html>
<head>
<script>
function trimString(stringValue) {
return stringValue.replace(/^\s+|\s+$/g, '');
};
window.addEventListener("message", function(e) {
sendData = null;
@ -61,7 +65,16 @@ window.addEventListener("message", function(e) {
res.responseHeaders[responseHeader] =
xhr.getResponseHeader(responseHeader);
}
res.allResponseHeaders = {};
var splitHeaders = xhr.getAllResponseHeaders().split("\r\n");
for (var i = 0; i < splitHeaders.length; i++) {
var headerValuePair = splitHeaders[i].split(":");
if(headerValuePair[1] != null) {
var headerName = trimString(headerValuePair[0]);
var headerValue = trimString(headerValuePair[1]);
res.allResponseHeaders[headerName] = headerValue;
}
}
post(e, res);
}

View File

@ -50,7 +50,16 @@ window.addEventListener("message", function(e) {\n\
res.responseHeaders[responseHeader] =\n\
xhr.getResponseHeader(responseHeader);\n\
}\n\
\n\
res.allResponseHeaders = {};\n\
var splitHeaders = xhr.getAllResponseHeaders().split("\\r\\n");\n\
for (var i = 0; i < splitHeaders.length; i++) {\n\
var headerValuePair = splitHeaders[i].split(":");\n\
if(headerValuePair[1] != null){\n\
var headerName = trimString(headerValuePair[0]);\n\
var headerValue = trimString(headerValuePair[1]); \n\
res.allResponseHeaders[headerName] = headerValue;\n\
}\n\
}\n\
post(e, res);\n\
}\n\
\n\
@ -74,6 +83,9 @@ window.addEventListener("message", function(e) {\n\
function post(e, res) {\n\
e.source.postMessage(res.toSource(), "*");\n\
}\n\
function trimString(stringValue) {\n\
return stringValue.replace("/^\s+|\s+$/g","");\n\
};\n\
\n\
</script>\n\
</head>\n\

View File

@ -559,7 +559,16 @@ function runTest() {
exposeHeaders: " , ,,y-my-header,z-my-header, ",
expectedResponseHeaders: ["y-my-header"],
},
{ pass: 1,
method: "GET",
responseHeaders: { "Cache-Control": "cacheControl header",
"Content-Language": "contentLanguage header",
"Expires":"expires header",
"Last-Modified":"lastModified header",
"Pragma":"pragma header",
"Unexpected":"unexpected header" },
expectedResponseHeaders: ["Cache-Control","Content-Language","Content-Type","Expires","Last-Modified","Pragma"],
},
// Check that sending a body in the OPTIONS response works
{ pass: 1,
method: "DELETE",
@ -673,13 +682,19 @@ function runTest() {
for (header in test.responseHeaders) {
if (test.expectedResponseHeaders.indexOf(header) == -1) {
is(res.responseHeaders[header], null,
"wrong response header (" + header + ") in test for " +
"|xhr.getResponseHeader()|wrong response header (" + header + ") in test for " +
test.toSource());
is(res.allResponseHeaders[header], null,
"|xhr.getAllResponseHeaderss()|wrong response header (" + header + ") in test for " +
test.toSource());
}
else {
is(res.responseHeaders[header], test.responseHeaders[header],
"wrong response header (" + header + ") in test for " +
"|xhr.getResponseHeader()|wrong response header (" + header + ") in test for " +
test.toSource());
is(res.allResponseHeaders[header], test.responseHeaders[header],
"|xhr.getAllResponseHeaderss()|wrong response header (" + header + ") in test for " +
test.toSource());
}
}
}