diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 2553624cd4b2..b43ece99b6d6 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -7071,8 +7071,9 @@ nsContentUtils::IsForbiddenSystemRequestHeader(const nsACString& aHeader) static const char *kInvalidHeaders[] = { "accept-charset", "accept-encoding", "access-control-request-headers", "access-control-request-method", "connection", "content-length", - "cookie", "cookie2", "date", "dnt", "expect", "host", "keep-alive", - "origin", "referer", "te", "trailer", "transfer-encoding", "upgrade", "via" + "cookie", "cookie2", "content-transfer-encoding", "date", "dnt", + "expect", "host", "keep-alive", "origin", "referer", "te", "trailer", + "transfer-encoding", "upgrade", "via" }; for (uint32_t i = 0; i < ArrayLength(kInvalidHeaders); ++i) { if (aHeader.LowerCaseEqualsASCII(kInvalidHeaders[i])) { diff --git a/dom/xhr/XMLHttpRequestMainThread.cpp b/dom/xhr/XMLHttpRequestMainThread.cpp index 4c7b33c0d3da..7ee499f9ea3d 100644 --- a/dom/xhr/XMLHttpRequestMainThread.cpp +++ b/dom/xhr/XMLHttpRequestMainThread.cpp @@ -1465,7 +1465,7 @@ XMLHttpRequestMainThread::Open(const nsACString& inMethod, const nsACString& url // Clear our record of previously set headers so future header set // operations will merge/override correctly. - mAuthorRequestHeaders.Clear(); + mAlreadySetHeaders.Clear(); // When we are called from JS we can find the load group for the page, // and add ourselves to it. This way any pending requests @@ -2480,9 +2480,6 @@ XMLHttpRequestMainThread::Send(nsIVariant* aVariant, const Nullable nsCOMPtr httpChannel(do_QueryInterface(mChannel)); if (httpChannel) { - // Spec step 5 - SetAuthorRequestHeadersOnChannel(httpChannel); - httpChannel->GetRequestMethod(method); // If GET, method name will be uppercase if (!IsSystemXHR()) { @@ -2537,14 +2534,15 @@ XMLHttpRequestMainThread::Send(nsIVariant* aVariant, const Nullable net::InScriptableRange(size_u64) ? static_cast(size_u64) : -1; if (postDataStream) { - // If author set no Content-Type, use the default from GetRequestBody(). + // If no content type header was set by the client, we set it to + // application/xml. nsAutoCString contentType; - - GetAuthorRequestHeaderValue("content-type", contentType); - if (contentType.IsVoid()) { + if (NS_FAILED(httpChannel-> + GetRequestHeader(NS_LITERAL_CSTRING("Content-Type"), + contentType))) { contentType = defaultContentType; - if (!charset.IsEmpty()) { + if (!charset.IsEmpty() && !contentType.IsVoid()) { // If we are providing the default content type, then we also need to // provide a charset declaration. contentType.Append(NS_LITERAL_CSTRING(";charset=")); @@ -2722,26 +2720,8 @@ XMLHttpRequestMainThread::Send(nsIVariant* aVariant, const Nullable // Set up the preflight if needed if (!IsSystemXHR()) { - nsTArray CORSUnsafeHeaders; - const char *kCrossOriginSafeHeaders[] = { - "accept", "accept-language", "content-language", "content-type", - "last-event-id" - }; - for (RequestHeader& header : mAuthorRequestHeaders) { - bool safe = false; - for (uint32_t i = 0; i < ArrayLength(kCrossOriginSafeHeaders); ++i) { - if (header.name.EqualsASCII(kCrossOriginSafeHeaders[i])) { - safe = true; - break; - } - } - if (!safe) { - CORSUnsafeHeaders.AppendElement(header.name); - } - } - nsCOMPtr loadInfo = mChannel->GetLoadInfo(); - loadInfo->SetCorsPreflightInfo(CORSUnsafeHeaders, + loadInfo->SetCorsPreflightInfo(mCORSUnsafeHeaders, mFlagHadUploadListenersOnSend); } @@ -2867,62 +2847,99 @@ XMLHttpRequestMainThread::Send(nsIVariant* aVariant, const Nullable // http://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html#dom-xmlhttprequest-setrequestheader NS_IMETHODIMP -XMLHttpRequestMainThread::SetRequestHeader(const nsACString& aName, - const nsACString& aValue) +XMLHttpRequestMainThread::SetRequestHeader(const nsACString& header, + const nsACString& value) { // Steps 1 and 2 if (mState != State::opened || mFlagSend) { return NS_ERROR_DOM_INVALID_STATE_ERR; } + NS_ASSERTION(mChannel, "mChannel must be valid if we're OPENED."); // Step 3 - nsAutoCString value(aValue); - static const char kHTTPWhitespace[] = "\n\t\r "; - value.Trim(kHTTPWhitespace); - - // Step 4 - if (!NS_IsValidHTTPToken(aName) || !NS_IsReasonableHTTPHeaderValue(value)) { + // Make sure we don't store an invalid header name in mCORSUnsafeHeaders + if (!NS_IsValidHTTPToken(header)) { return NS_ERROR_DOM_SYNTAX_ERR; } - // Step 5 - bool isPrivilegedCaller = IsSystemXHR(); - bool isForbiddenHeader = nsContentUtils::IsForbiddenRequestHeader(aName); - if (!isPrivilegedCaller && isForbiddenHeader) { - NS_WARNING("refusing to set request header"); + if (!mChannel) // open() initializes mChannel, and open() + return NS_ERROR_FAILURE; // must be called before first setRequestHeader() + + nsCOMPtr httpChannel = do_QueryInterface(mChannel); + if (!httpChannel) { return NS_OK; } - // Step 6.1 - nsAutoCString lowercaseName; - nsContentUtils::ASCIIToLower(aName, lowercaseName); + // We will merge XHR headers, per the spec (secion 4.6.2) unless: + // 1 - The caller is privileged and setting an invalid header, + // or + // 2 - we have not yet explicitly set that header; this allows web + // content to override default headers the first time they set them. + bool mergeHeaders = true; - // Step 6.2 - bool notAlreadySet = true; - for (RequestHeader& header : mAuthorRequestHeaders) { - if (header.name.Equals(lowercaseName)) { - // Gecko-specific: invalid headers can be set by privileged - // callers, but will not merge. - if (isPrivilegedCaller && isForbiddenHeader) { - header.value.Assign(value); - } else { - header.value.AppendLiteral(", "); - header.value.Append(value); + if (!IsSystemXHR()) { + // Step 5: Check for dangerous headers. + // Prevent modification to certain HTTP headers (see bug 302263), unless + // the executing script is privileged. + if (nsContentUtils::IsForbiddenRequestHeader(header)) { + NS_WARNING("refusing to set request header"); + return NS_OK; + } + + // Check for dangerous cross-site headers + bool safeHeader = IsSystemXHR(); + if (!safeHeader) { + // Content-Type isn't always safe, but we'll deal with it in Send() + const char *kCrossOriginSafeHeaders[] = { + "accept", "accept-language", "content-language", "content-type", + "last-event-id" + }; + for (uint32_t i = 0; i < ArrayLength(kCrossOriginSafeHeaders); ++i) { + if (header.LowerCaseEqualsASCII(kCrossOriginSafeHeaders[i])) { + safeHeader = true; + break; + } } - notAlreadySet = false; - break; + } + + if (!safeHeader) { + if (!mCORSUnsafeHeaders.Contains(header, nsCaseInsensitiveCStringArrayComparator())) { + mCORSUnsafeHeaders.AppendElement(header); + } + } + } else { + // Case 1 above + if (nsContentUtils::IsForbiddenSystemRequestHeader(header)) { + mergeHeaders = false; } } - // Step 6.3 - if (notAlreadySet) { - RequestHeader newHeader = { - nsCString(lowercaseName), nsCString(value) - }; - mAuthorRequestHeaders.AppendElement(newHeader); + if (!mAlreadySetHeaders.Contains(header)) { + // Case 2 above + mergeHeaders = false; } - return NS_OK; + nsresult rv; + if (value.IsEmpty()) { + rv = httpChannel->SetEmptyRequestHeader(header); + } else { + // Merge headers depending on what we decided above. + rv = httpChannel->SetRequestHeader(header, value, mergeHeaders); + } + if (rv == NS_ERROR_INVALID_ARG) { + return NS_ERROR_DOM_SYNTAX_ERR; + } + if (NS_SUCCEEDED(rv)) { + // Remember that we've set this header, so subsequent set operations will merge values. + mAlreadySetHeaders.PutEntry(nsCString(header)); + + // We'll want to duplicate this header for any replacement channels (eg. on redirect) + RequestHeader reqHeader = { + nsCString(header), nsCString(value) + }; + mModifiedRequestHeaders.AppendElement(reqHeader); + } + return rv; } NS_IMETHODIMP @@ -3162,32 +3179,6 @@ XMLHttpRequestMainThread::AsyncOnChannelRedirect(nsIChannel *aOldChannel, return NS_OK; } -void -XMLHttpRequestMainThread::GetAuthorRequestHeaderValue(const char* aName, - nsACString& outValue) -{ - for (RequestHeader& header : mAuthorRequestHeaders) { - if (header.name.Equals(aName)) { - outValue.Assign(header.value); - return; - } - } - outValue.SetIsVoid(true); -} - -void -XMLHttpRequestMainThread::SetAuthorRequestHeadersOnChannel( - nsCOMPtr aHttpChannel) -{ - for (RequestHeader& header : mAuthorRequestHeaders) { - if (header.value.IsEmpty()) { - aHttpChannel->SetEmptyRequestHeader(header.name); - } else { - aHttpChannel->SetRequestHeader(header.name, header.value, false); - } - } -} - nsresult XMLHttpRequestMainThread::OnRedirectVerifyCallback(nsresult result) { @@ -3200,7 +3191,15 @@ XMLHttpRequestMainThread::OnRedirectVerifyCallback(nsresult result) nsCOMPtr httpChannel(do_QueryInterface(mChannel)); if (httpChannel) { // Ensure all original headers are duplicated for the new channel (bug #553888) - SetAuthorRequestHeadersOnChannel(httpChannel); + for (RequestHeader& requestHeader : mModifiedRequestHeaders) { + if (requestHeader.value.IsEmpty()) { + httpChannel->SetEmptyRequestHeader(requestHeader.header); + } else { + httpChannel->SetRequestHeader(requestHeader.header, + requestHeader.value, + false); + } + } } } else { mErrorLoad = true; @@ -3516,8 +3515,9 @@ XMLHttpRequestMainThread::ShouldBlockAuthPrompt() // Verify that it's ok to prompt for credentials here, per spec // http://xhr.spec.whatwg.org/#the-send%28%29-method - for (RequestHeader& requestHeader : mAuthorRequestHeaders) { - if (requestHeader.name.EqualsLiteral("authorization")) { + for (uint32_t i = 0, len = mModifiedRequestHeaders.Length(); i < len; ++i) { + if (mModifiedRequestHeaders[i].header. + LowerCaseEqualsLiteral("authorization")) { return true; } } diff --git a/dom/xhr/XMLHttpRequestMainThread.h b/dom/xhr/XMLHttpRequestMainThread.h index d02c522a54d0..9a6138250a8e 100644 --- a/dom/xhr/XMLHttpRequestMainThread.h +++ b/dom/xhr/XMLHttpRequestMainThread.h @@ -220,10 +220,10 @@ public: } virtual void - SetRequestHeader(const nsACString& aName, const nsACString& aValue, + SetRequestHeader(const nsACString& aHeader, const nsACString& aValue, ErrorResult& aRv) override { - aRv = SetRequestHeader(aName, aValue); + aRv = SetRequestHeader(aHeader, aValue); } virtual uint32_t @@ -620,6 +620,7 @@ protected: nsCOMPtr mPrincipal; nsCOMPtr mChannel; nsCOMPtr mResponseXML; + nsTArray mCORSUnsafeHeaders; nsCOMPtr mXMLParserStreamListener; @@ -790,13 +791,12 @@ protected: struct RequestHeader { - nsCString name; + nsCString header; nsCString value; }; - nsTArray mAuthorRequestHeaders; + nsTArray mModifiedRequestHeaders; - void GetAuthorRequestHeaderValue(const char* aName, nsACString& outValue); - void SetAuthorRequestHeadersOnChannel(nsCOMPtr aChannel); + nsTHashtable mAlreadySetHeaders; // Helper object to manage our XPCOM scriptability bits nsXMLHttpRequestXPCOMifier* mXPCOMifier; diff --git a/dom/xhr/tests/test_xhr_forbidden_headers.html b/dom/xhr/tests/test_xhr_forbidden_headers.html index fc076731ec7a..4ec8ee03e506 100644 --- a/dom/xhr/tests/test_xhr_forbidden_headers.html +++ b/dom/xhr/tests/test_xhr_forbidden_headers.html @@ -28,6 +28,7 @@ var headers = [ "coNtEnt-LEngth", "CoOKIe", "cOOkiE2", + "cOntEnt-tRAnsFer-enCoDiNg", "DATE", "dNT", "exPeCt", @@ -53,7 +54,6 @@ function startTest() { request.open("GET", window.location.href); for (i = 0; i < headers.length; i++) request.setRequestHeader(headers[i], "test" + i); - request.send(); // headers aren't set on the channel until send() // Read out headers var channel = SpecialPowers.wrap(request).channel.QueryInterface(SpecialPowers.Ci.nsIHttpChannel); @@ -73,7 +73,6 @@ function startTest() { request.open("GET", window.location.href); for (i = 0; i < headers.length; i++) request.setRequestHeader(headers[i], "test" + i); - request.send(); // headers aren't set on the channel until send() // Read out headers var channel = SpecialPowers.wrap(request).channel.QueryInterface(SpecialPowers.Ci.nsIHttpChannel); diff --git a/testing/web-platform/meta/XMLHttpRequest/setrequestheader-case-insensitive.htm.ini b/testing/web-platform/meta/XMLHttpRequest/setrequestheader-case-insensitive.htm.ini new file mode 100644 index 000000000000..16277476d3ab --- /dev/null +++ b/testing/web-platform/meta/XMLHttpRequest/setrequestheader-case-insensitive.htm.ini @@ -0,0 +1,5 @@ +[setrequestheader-case-insensitive.htm] + type: testharness + [XMLHttpRequest: setRequestHeader() - headers that differ in case] + expected: FAIL + diff --git a/testing/web-platform/meta/XMLHttpRequest/setrequestheader-header-allowed.htm.ini b/testing/web-platform/meta/XMLHttpRequest/setrequestheader-header-allowed.htm.ini new file mode 100644 index 000000000000..0a2cae6870a4 --- /dev/null +++ b/testing/web-platform/meta/XMLHttpRequest/setrequestheader-header-allowed.htm.ini @@ -0,0 +1,14 @@ +[setrequestheader-header-allowed.htm] + type: testharness + [XMLHttpRequest: setRequestHeader() - headers that are allowed (Authorization)] + expected: FAIL + + [XMLHttpRequest: setRequestHeader() - headers that are allowed (Content-Transfer-Encoding)] + expected: FAIL + + [XMLHttpRequest: setRequestHeader() - headers that are allowed (Content-Type)] + expected: FAIL + + [XMLHttpRequest: setRequestHeader() - headers that are allowed (User-Agent)] + expected: FAIL +