diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index b312df124a6e..69d1dee3224f 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -7067,9 +7067,8 @@ 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", "content-transfer-encoding", "date", "dnt", - "expect", "host", "keep-alive", "origin", "referer", "te", "trailer", - "transfer-encoding", "upgrade", "via" + "cookie", "cookie2", "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 7ee499f9ea3d..4c7b33c0d3da 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. - mAlreadySetHeaders.Clear(); + mAuthorRequestHeaders.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,6 +2480,9 @@ 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()) { @@ -2534,15 +2537,14 @@ XMLHttpRequestMainThread::Send(nsIVariant* aVariant, const Nullable net::InScriptableRange(size_u64) ? static_cast(size_u64) : -1; if (postDataStream) { - // If no content type header was set by the client, we set it to - // application/xml. + // If author set no Content-Type, use the default from GetRequestBody(). nsAutoCString contentType; - if (NS_FAILED(httpChannel-> - GetRequestHeader(NS_LITERAL_CSTRING("Content-Type"), - contentType))) { + + GetAuthorRequestHeaderValue("content-type", contentType); + if (contentType.IsVoid()) { contentType = defaultContentType; - if (!charset.IsEmpty() && !contentType.IsVoid()) { + if (!charset.IsEmpty()) { // If we are providing the default content type, then we also need to // provide a charset declaration. contentType.Append(NS_LITERAL_CSTRING(";charset=")); @@ -2720,8 +2722,26 @@ 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(mCORSUnsafeHeaders, + loadInfo->SetCorsPreflightInfo(CORSUnsafeHeaders, mFlagHadUploadListenersOnSend); } @@ -2847,99 +2867,62 @@ 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& header, - const nsACString& value) +XMLHttpRequestMainThread::SetRequestHeader(const nsACString& aName, + const nsACString& aValue) { // 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 - // Make sure we don't store an invalid header name in mCORSUnsafeHeaders - if (!NS_IsValidHTTPToken(header)) { + nsAutoCString value(aValue); + static const char kHTTPWhitespace[] = "\n\t\r "; + value.Trim(kHTTPWhitespace); + + // Step 4 + if (!NS_IsValidHTTPToken(aName) || !NS_IsReasonableHTTPHeaderValue(value)) { return NS_ERROR_DOM_SYNTAX_ERR; } - if (!mChannel) // open() initializes mChannel, and open() - return NS_ERROR_FAILURE; // must be called before first setRequestHeader() - - nsCOMPtr httpChannel = do_QueryInterface(mChannel); - if (!httpChannel) { + // Step 5 + bool isPrivilegedCaller = IsSystemXHR(); + bool isForbiddenHeader = nsContentUtils::IsForbiddenRequestHeader(aName); + if (!isPrivilegedCaller && isForbiddenHeader) { + NS_WARNING("refusing to set request header"); return NS_OK; } - // 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.1 + nsAutoCString lowercaseName; + nsContentUtils::ASCIIToLower(aName, lowercaseName); - 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; - } + // 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 (!safeHeader) { - if (!mCORSUnsafeHeaders.Contains(header, nsCaseInsensitiveCStringArrayComparator())) { - mCORSUnsafeHeaders.AppendElement(header); - } - } - } else { - // Case 1 above - if (nsContentUtils::IsForbiddenSystemRequestHeader(header)) { - mergeHeaders = false; + notAlreadySet = false; + break; } } - if (!mAlreadySetHeaders.Contains(header)) { - // Case 2 above - mergeHeaders = false; - } - - 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) + // Step 6.3 + if (notAlreadySet) { + RequestHeader newHeader = { + nsCString(lowercaseName), nsCString(value) }; - mModifiedRequestHeaders.AppendElement(reqHeader); + mAuthorRequestHeaders.AppendElement(newHeader); } - return rv; + + return NS_OK; } NS_IMETHODIMP @@ -3179,6 +3162,32 @@ 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) { @@ -3191,15 +3200,7 @@ XMLHttpRequestMainThread::OnRedirectVerifyCallback(nsresult result) nsCOMPtr httpChannel(do_QueryInterface(mChannel)); if (httpChannel) { // Ensure all original headers are duplicated for the new channel (bug #553888) - for (RequestHeader& requestHeader : mModifiedRequestHeaders) { - if (requestHeader.value.IsEmpty()) { - httpChannel->SetEmptyRequestHeader(requestHeader.header); - } else { - httpChannel->SetRequestHeader(requestHeader.header, - requestHeader.value, - false); - } - } + SetAuthorRequestHeadersOnChannel(httpChannel); } } else { mErrorLoad = true; @@ -3515,9 +3516,8 @@ 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 (uint32_t i = 0, len = mModifiedRequestHeaders.Length(); i < len; ++i) { - if (mModifiedRequestHeaders[i].header. - LowerCaseEqualsLiteral("authorization")) { + for (RequestHeader& requestHeader : mAuthorRequestHeaders) { + if (requestHeader.name.EqualsLiteral("authorization")) { return true; } } diff --git a/dom/xhr/XMLHttpRequestMainThread.h b/dom/xhr/XMLHttpRequestMainThread.h index 9a6138250a8e..d02c522a54d0 100644 --- a/dom/xhr/XMLHttpRequestMainThread.h +++ b/dom/xhr/XMLHttpRequestMainThread.h @@ -220,10 +220,10 @@ public: } virtual void - SetRequestHeader(const nsACString& aHeader, const nsACString& aValue, + SetRequestHeader(const nsACString& aName, const nsACString& aValue, ErrorResult& aRv) override { - aRv = SetRequestHeader(aHeader, aValue); + aRv = SetRequestHeader(aName, aValue); } virtual uint32_t @@ -620,7 +620,6 @@ protected: nsCOMPtr mPrincipal; nsCOMPtr mChannel; nsCOMPtr mResponseXML; - nsTArray mCORSUnsafeHeaders; nsCOMPtr mXMLParserStreamListener; @@ -791,12 +790,13 @@ protected: struct RequestHeader { - nsCString header; + nsCString name; nsCString value; }; - nsTArray mModifiedRequestHeaders; + nsTArray mAuthorRequestHeaders; - nsTHashtable mAlreadySetHeaders; + void GetAuthorRequestHeaderValue(const char* aName, nsACString& outValue); + void SetAuthorRequestHeadersOnChannel(nsCOMPtr aChannel); // 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 4ec8ee03e506..fc076731ec7a 100644 --- a/dom/xhr/tests/test_xhr_forbidden_headers.html +++ b/dom/xhr/tests/test_xhr_forbidden_headers.html @@ -28,7 +28,6 @@ var headers = [ "coNtEnt-LEngth", "CoOKIe", "cOOkiE2", - "cOntEnt-tRAnsFer-enCoDiNg", "DATE", "dNT", "exPeCt", @@ -54,6 +53,7 @@ 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,6 +73,7 @@ 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 deleted file mode 100644 index 16277476d3ab..000000000000 --- a/testing/web-platform/meta/XMLHttpRequest/setrequestheader-case-insensitive.htm.ini +++ /dev/null @@ -1,5 +0,0 @@ -[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 deleted file mode 100644 index 0a2cae6870a4..000000000000 --- a/testing/web-platform/meta/XMLHttpRequest/setrequestheader-header-allowed.htm.ini +++ /dev/null @@ -1,14 +0,0 @@ -[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 -