diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 668e20780e..e484abcf20 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -7058,16 +7058,26 @@ nsContentUtils::HasDistributedChildren(nsIContent* aContent) // static bool -nsContentUtils::IsForbiddenRequestHeader(const nsACString& aHeader) +nsContentUtils::IsForbiddenRequestHeader(const nsACString& aHeader, + const nsACString& aValue) { if (IsForbiddenSystemRequestHeader(aHeader)) { return true; } + + if ((nsContentUtils::IsOverrideMethodHeader(aHeader) && + nsContentUtils::ContainsForbiddenMethod(aValue))) { + return true; + } - return StringBeginsWith(aHeader, NS_LITERAL_CSTRING("proxy-"), - nsCaseInsensitiveCStringComparator()) || - StringBeginsWith(aHeader, NS_LITERAL_CSTRING("sec-"), - nsCaseInsensitiveCStringComparator()); + if (StringBeginsWith(aHeader, NS_LITERAL_CSTRING("proxy-"), + nsCaseInsensitiveCStringComparator()) || + StringBeginsWith(aHeader, NS_LITERAL_CSTRING("sec-"), + nsCaseInsensitiveCStringComparator())) { + return true; + } + + return false; } // static @@ -7096,6 +7106,64 @@ nsContentUtils::IsForbiddenResponseHeader(const nsACString& aHeader) aHeader.LowerCaseEqualsASCII("set-cookie2")); } +// static +bool +nsContentUtils::IsOverrideMethodHeader(const nsACString& headerName) { + return headerName.LowerCaseEqualsASCII("x-http-method-override") || + headerName.LowerCaseEqualsASCII("x-http-method") || + headerName.LowerCaseEqualsASCII("x-method-override"); +} + +// static +bool +nsContentUtils::ContainsForbiddenMethod(const nsACString& headerValue) { + bool hasInsecureMethod = false; + nsCCharSeparatedTokenizer tokenizer(headerValue, ','); + + while (tokenizer.hasMoreTokens()) { + const nsDependentCSubstring& value = tokenizer.nextToken(); + + if (value.LowerCaseEqualsASCII("connect") || + value.LowerCaseEqualsASCII("trace") || + value.LowerCaseEqualsASCII("track")) { + hasInsecureMethod = true; + break; + } + } + + return hasInsecureMethod; +} + +// static +bool nsContentUtils::IsCorsUnsafeRequestHeaderValue( + const nsACString& aHeaderValue) { + const char* cur = aHeaderValue.BeginReading(); + const char* end = aHeaderValue.EndReading(); + + while (cur != end) { + // Implementation of + // https://fetch.spec.whatwg.org/#cors-unsafe-request-header-byte Is less + // than a space but not a horizontal tab + if ((*cur < ' ' && *cur != '\t') || *cur == '"' || *cur == '(' || + *cur == ')' || *cur == ':' || *cur == '<' || *cur == '>' || + *cur == '?' || *cur == '@' || *cur == '[' || *cur == '\\' || + *cur == ']' || *cur == '{' || *cur == '}' || + *cur == 0x7F) { // 0x75 is DEL + return true; + } + cur++; + } + return false; +} + +// static +bool nsContentUtils::IsAllowedNonCorsAccept(const nsACString& aHeaderValue) { + if (IsCorsUnsafeRequestHeaderValue(aHeaderValue)) { + return false; + } + return true; +} + // static bool nsContentUtils::IsAllowedNonCorsContentType(const nsACString& aHeaderValue) @@ -7103,6 +7171,10 @@ nsContentUtils::IsAllowedNonCorsContentType(const nsACString& aHeaderValue) nsAutoCString contentType; nsAutoCString unused; + if (IsCorsUnsafeRequestHeaderValue(aHeaderValue)) { + return false; + } + nsresult rv = NS_ParseRequestContentType(aHeaderValue, contentType, unused); if (NS_FAILED(rv)) { return false; @@ -7113,6 +7185,41 @@ nsContentUtils::IsAllowedNonCorsContentType(const nsACString& aHeaderValue) contentType.LowerCaseEqualsLiteral("multipart/form-data"); } +// static +bool nsContentUtils::IsAllowedNonCorsLanguage(const nsACString& aHeaderValue) { + const char* cur = aHeaderValue.BeginReading(); + const char* end = aHeaderValue.EndReading(); + + while (cur != end) { + if ((*cur >= '0' && *cur <= '9') || (*cur >= 'A' && *cur <= 'Z') || + (*cur >= 'a' && *cur <= 'z') || *cur == ' ' || *cur == '*' || + *cur == ',' || *cur == '-' || *cur == '.' || *cur == ';' || + *cur == '=') { + cur++; + continue; + } + return false; + } + return true; +} + +// static +bool nsContentUtils::IsCORSSafelistedRequestHeader(const nsACString& aName, + const nsACString& aValue) { + // See https://fetch.spec.whatwg.org/#cors-safelisted-request-header + if (aValue.Length() > 128) { + return false; + } + return (aName.LowerCaseEqualsLiteral("accept") && + nsContentUtils::IsAllowedNonCorsAccept(aValue)) || + (aName.LowerCaseEqualsLiteral("accept-language") && + nsContentUtils::IsAllowedNonCorsLanguage(aValue)) || + (aName.LowerCaseEqualsLiteral("content-language") && + nsContentUtils::IsAllowedNonCorsLanguage(aValue)) || + (aName.LowerCaseEqualsLiteral("content-type") && + nsContentUtils::IsAllowedNonCorsContentType(aValue)); +} + bool nsContentUtils::DOMWindowDumpEnabled() { diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h index b3bc22f286..8597e137ef 100644 --- a/dom/base/nsContentUtils.h +++ b/dom/base/nsContentUtils.h @@ -2425,7 +2425,8 @@ public: * Returns whether a given header is forbidden for an XHR or fetch * request. */ - static bool IsForbiddenRequestHeader(const nsACString& aHeader); + static bool IsForbiddenRequestHeader(const nsACString& aHeader, + const nsACString& aValue); /** * Returns whether a given header is forbidden for a system XHR @@ -2433,18 +2434,50 @@ public: */ static bool IsForbiddenSystemRequestHeader(const nsACString& aHeader); + /** + * Returns whether a given header has characters that aren't permitted + */ + static bool IsCorsUnsafeRequestHeaderValue(const nsACString& aHeaderValue); + + /** + * Returns whether a given Accept header value is allowed + * for a non-CORS XHR or fetch request. + */ + static bool IsAllowedNonCorsAccept(const nsACString& aHeaderValue); + /** * Returns whether a given Content-Type header value is allowed * for a non-CORS XHR or fetch request. */ static bool IsAllowedNonCorsContentType(const nsACString& aHeaderValue); + /** + * Returns whether a given Content-Language or accept-language header value is + * allowed for a non-CORS XHR or fetch request. + */ + static bool IsAllowedNonCorsLanguage(const nsACString& aHeaderValue); + + /** + * Returns whether a given header and value is a CORS-safelisted request + * header per https://fetch.spec.whatwg.org/#cors-safelisted-request-header + */ + static bool IsCORSSafelistedRequestHeader(const nsACString& aName, + const nsACString& aValue); + /** * Returns whether a given header is forbidden for an XHR or fetch * response. */ static bool IsForbiddenResponseHeader(const nsACString& aHeader); + /** + * Checks whether the header overrides any http methods + */ + static bool IsOverrideMethodHeader(const nsACString& headerName); + /** + * Checks whether the header value contains any forbidden method + */ + static bool ContainsForbiddenMethod(const nsACString& headerValue); /** * Returns the inner window ID for the window associated with a request, */ diff --git a/dom/fetch/InternalHeaders.cpp b/dom/fetch/InternalHeaders.cpp index 0448d84731..f4851b7064 100644 --- a/dom/fetch/InternalHeaders.cpp +++ b/dom/fetch/InternalHeaders.cpp @@ -5,6 +5,7 @@ #include "mozilla/dom/InternalHeaders.h" +#include "FetchUtil.h" #include "mozilla/dom/FetchTypes.h" #include "mozilla/ErrorResult.h" @@ -46,20 +47,112 @@ InternalHeaders::ToIPC(nsTArray& aIPCHeaders, } } +bool +InternalHeaders::IsValidHeaderValue(const nsCString& aLowerName, + const nsCString& aNormalizedValue, + ErrorResult& aRv) { + // Steps 2 to 6 for ::Set() and ::Append() in the spec. + + // Step 2 + if (IsInvalidName(aLowerName, aRv) || IsInvalidValue(aNormalizedValue, aRv)) { + return false; + } + + // Step 3 + if (IsImmutable(aRv)) { + return false; + } + + // Step 4 + if (mGuard == HeadersGuardEnum::Request) { + if (IsForbiddenRequestHeader(aLowerName, aNormalizedValue)) { + return false; + } + } + + // Step 5 + if (mGuard == HeadersGuardEnum::Request_no_cors) { + nsAutoCString tempValue; + Get(aLowerName, tempValue, aRv); + + if (tempValue.IsVoid()) { + tempValue = aNormalizedValue; + } else { + tempValue.Append(", "); + tempValue.Append(aNormalizedValue); + } + + if (!nsContentUtils::IsCORSSafelistedRequestHeader(aLowerName, tempValue)) { + return false; + } + } + + // Step 6 + else if (IsForbiddenResponseHeader(aLowerName)) { + return false; + } + + return true; +} + void InternalHeaders::Append(const nsACString& aName, const nsACString& aValue, ErrorResult& aRv) { + // Step 1 + nsAutoCString trimValue; + NS_TrimHTTPWhitespace(aValue, trimValue); + + // Steps 2 to 6 nsAutoCString lowerName; ToLowerCase(aName, lowerName); - - if (IsInvalidMutableHeader(lowerName, aValue, aRv)) { + if (!IsValidHeaderValue(lowerName, trimValue, aRv)) { return; } + // Step 7 SetListDirty(); - mList.AppendElement(Entry(lowerName, aValue)); + + // Step 8 + if (mGuard == HeadersGuardEnum::Request_no_cors) { + RemovePrivilegedNoCorsRequestHeaders(); + } +} + +void InternalHeaders::RemovePrivilegedNoCorsRequestHeaders() { + bool dirty = false; + + // remove in reverse order to minimize copying + for (int32_t i = mList.Length() - 1; i >= 0; --i) { + if (IsPrivilegedNoCorsRequestHeaderName(mList[i].mName)) { + mList.RemoveElementAt(i); + dirty = true; + } + } + + if (dirty) { + SetListDirty(); + } +} + +bool InternalHeaders::DeleteInternal(const nsCString& aLowerName, + ErrorResult& aRv) { + bool dirty = false; + + // remove in reverse order to minimize copying + for (int32_t i = mList.Length() - 1; i >= 0; --i) { + if (mList[i].mName.EqualsIgnoreCase(aLowerName.get())) { + mList.RemoveElementAt(i); + dirty = true; + } + } + + if (dirty) { + SetListDirty(); + } + + return dirty; } void @@ -68,17 +161,43 @@ InternalHeaders::Delete(const nsACString& aName, ErrorResult& aRv) nsAutoCString lowerName; ToLowerCase(aName, lowerName); - if (IsInvalidMutableHeader(lowerName, aRv)) { + // Step 1 + if (IsInvalidName(lowerName, aRv)) { return; } - SetListDirty(); + // Step 2 + if (IsImmutable(aRv)) { + return; + } - // remove in reverse order to minimize copying - for (int32_t i = mList.Length() - 1; i >= 0; --i) { - if (lowerName == mList[i].mName) { - mList.RemoveElementAt(i); - } + // Step 3 + nsAutoCString value; + GetInternal(lowerName, value, aRv); + if (IsForbiddenRequestHeader(lowerName, value)) { + return; + } + + // Step 4 + if (mGuard == HeadersGuardEnum::Request_no_cors && + !IsNoCorsSafelistedRequestHeaderName(lowerName) && + !IsPrivilegedNoCorsRequestHeaderName(lowerName)) { + return; + } + + // Step 5 + if (IsForbiddenResponseHeader(lowerName)) { + return; + } + + // Steps 6 and 7 + if (!DeleteInternal(lowerName, aRv)) { + return; + } + + // Step 8 + if (mGuard == HeadersGuardEnum::Request_no_cors) { + RemovePrivilegedNoCorsRequestHeaders(); } } @@ -91,12 +210,18 @@ InternalHeaders::Get(const nsACString& aName, nsACString& aValue, ErrorResult& a if (IsInvalidName(lowerName, aRv)) { return; } + GetInternal(lowerName, aValue, aRv); +} +void +InternalHeaders::GetInternal(const nsCString& aLowerName, + nsACString& aValue, + ErrorResult& aRv) const { const char* delimiter = ","; bool firstValueFound = false; for (uint32_t i = 0; i < mList.Length(); ++i) { - if (lowerName == mList[i].mName) { + if (aLowerName == mList[i].mName) { if (firstValueFound) { aValue += delimiter; } @@ -153,13 +278,18 @@ InternalHeaders::Has(const nsACString& aName, ErrorResult& aRv) const void InternalHeaders::Set(const nsACString& aName, const nsACString& aValue, ErrorResult& aRv) { + // Step 1 + nsAutoCString trimValue; + NS_TrimHTTPWhitespace(aValue, trimValue); + + // Steps 2 to 6 nsAutoCString lowerName; ToLowerCase(aName, lowerName); - - if (IsInvalidMutableHeader(lowerName, aValue, aRv)) { + if (!IsValidHeaderValue(lowerName, trimValue, aRv)) { return; } + // Step 7 SetListDirty(); int32_t firstIndex = INT32_MAX; @@ -179,6 +309,11 @@ InternalHeaders::Set(const nsACString& aName, const nsACString& aValue, ErrorRes } else { mList.AppendElement(Entry(lowerName, aValue)); } + + // Step 8 + if (mGuard == HeadersGuardEnum::Request_no_cors) { + RemovePrivilegedNoCorsRequestHeaders(); + } } void @@ -200,6 +335,22 @@ InternalHeaders::~InternalHeaders() { } +// static +bool +InternalHeaders::IsNoCorsSafelistedRequestHeaderName(const nsCString& aName) { + return aName.EqualsIgnoreCase("accept") || + aName.EqualsIgnoreCase("accept-language") || + aName.EqualsIgnoreCase("content-language") || + aName.EqualsIgnoreCase("content-type"); +} + +// static +bool +InternalHeaders::IsPrivilegedNoCorsRequestHeaderName( + const nsCString& aName) { + return aName.EqualsIgnoreCase("range"); +} + // static bool InternalHeaders::IsSimpleHeader(const nsACString& aName, const nsACString& aValue) @@ -207,9 +358,12 @@ InternalHeaders::IsSimpleHeader(const nsACString& aName, const nsACString& aValu // Note, we must allow a null content-type value here to support // get("content-type"), but the IsInvalidValue() check will prevent null // from being set or appended. - return aName.EqualsLiteral("accept") || - aName.EqualsLiteral("accept-language") || - aName.EqualsLiteral("content-language") || + return (aName.EqualsLiteral("accept") && + nsContentUtils::IsAllowedNonCorsAccept(aValue)) || + (aName.EqualsLiteral("accept-language") && + nsContentUtils::IsAllowedNonCorsLanguage(aValue))|| + (aName.EqualsLiteral("content-language") && + nsContentUtils::IsAllowedNonCorsLanguage(aValue))|| (aName.EqualsLiteral("content-type") && nsContentUtils::IsAllowedNonCorsContentType(aValue)); } @@ -261,10 +415,11 @@ InternalHeaders::IsImmutable(ErrorResult& aRv) const } bool -InternalHeaders::IsForbiddenRequestHeader(const nsACString& aName) const +InternalHeaders::IsForbiddenRequestHeader(const nsACString& aName, + const nsACString& aValue) const { return mGuard == HeadersGuardEnum::Request && - nsContentUtils::IsForbiddenRequestHeader(aName); + nsContentUtils::IsForbiddenRequestHeader(aName, aValue); } bool @@ -370,7 +525,7 @@ InternalHeaders::CORSHeaders(InternalHeaders* aHeaders, RequestCredentials aCred ErrorResult result; nsAutoCString acExposedNames; - aHeaders->GetFirst(NS_LITERAL_CSTRING("Access-Control-Expose-Headers"), acExposedNames, result); + aHeaders->Get(NS_LITERAL_CSTRING("Access-Control-Expose-Headers"), acExposedNames, result); MOZ_ASSERT(!result.Failed()); bool allowAllHeaders = false; diff --git a/dom/fetch/InternalHeaders.h b/dom/fetch/InternalHeaders.h index ae9f2811a2..0a6996ab38 100644 --- a/dom/fetch/InternalHeaders.h +++ b/dom/fetch/InternalHeaders.h @@ -136,8 +136,11 @@ private: static bool IsInvalidName(const nsACString& aName, ErrorResult& aRv); static bool IsInvalidValue(const nsACString& aValue, ErrorResult& aRv); + bool IsValidHeaderValue(const nsCString& aLowerName, + const nsCString& aNormalizedValue, ErrorResult& aRv); bool IsImmutable(ErrorResult& aRv) const; - bool IsForbiddenRequestHeader(const nsACString& aName) const; + bool IsForbiddenRequestHeader(const nsACString& aName, + const nsACString& aValue) const; bool IsForbiddenRequestNoCorsHeader(const nsACString& aName) const; bool IsForbiddenRequestNoCorsHeader(const nsACString& aName, const nsACString& aValue) const; @@ -156,11 +159,22 @@ private: return IsInvalidName(aName, aRv) || IsInvalidValue(aValue, aRv) || IsImmutable(aRv) || - IsForbiddenRequestHeader(aName) || + IsForbiddenRequestHeader(aName, aValue) || IsForbiddenRequestNoCorsHeader(aName, aValue) || IsForbiddenResponseHeader(aName); } + void RemovePrivilegedNoCorsRequestHeaders(); + + void GetInternal(const nsCString& aLowerName, nsACString& aValue, + ErrorResult& aRv) const; + + bool DeleteInternal(const nsCString& aLowerName, ErrorResult& aRv); + + static bool IsNoCorsSafelistedRequestHeaderName(const nsCString& aName); + + static bool IsPrivilegedNoCorsRequestHeaderName(const nsCString& aName); + static bool IsSimpleHeader(const nsACString& aName, const nsACString& aValue); diff --git a/dom/xhr/XMLHttpRequestMainThread.cpp b/dom/xhr/XMLHttpRequestMainThread.cpp index f92ddd35ca..c5b174b5cb 100644 --- a/dom/xhr/XMLHttpRequestMainThread.cpp +++ b/dom/xhr/XMLHttpRequestMainThread.cpp @@ -3071,9 +3071,8 @@ XMLHttpRequestMainThread::SetRequestHeader(const nsACString& aName, } // Step 3 - nsAutoCString value(aValue); - static const char kHTTPWhitespace[] = "\n\t\r "; - value.Trim(kHTTPWhitespace); + nsAutoCString value; + NS_TrimHTTPWhitespace(aValue, value); // Step 4 if (!NS_IsValidHTTPToken(aName) || !NS_IsReasonableHTTPHeaderValue(value)) { @@ -3082,7 +3081,7 @@ XMLHttpRequestMainThread::SetRequestHeader(const nsACString& aName, // Step 5 bool isPrivilegedCaller = IsSystemXHR(); - bool isForbiddenHeader = nsContentUtils::IsForbiddenRequestHeader(aName); + bool isForbiddenHeader = nsContentUtils::IsForbiddenRequestHeader(aName, aValue); if (!isPrivilegedCaller && isForbiddenHeader) { NS_ConvertUTF8toUTF16 name(aName); const char16_t* params[] = { name.get() }; diff --git a/netwerk/base/nsNetUtil.cpp b/netwerk/base/nsNetUtil.cpp index ac6f620336..b9ec64ef84 100644 --- a/netwerk/base/nsNetUtil.cpp +++ b/netwerk/base/nsNetUtil.cpp @@ -520,6 +520,12 @@ bool NS_IsValidHTTPToken(const nsACString &aToken) return mozilla::net::nsHttp::IsValidToken(aToken); } +void +NS_TrimHTTPWhitespace(const nsACString& aSource, nsACString& aDest) +{ + mozilla::net::nsHttp::TrimHTTPWhitespace(aSource, aDest); +} + nsresult NS_NewLoadGroup(nsILoadGroup **aResult, nsIPrincipal *aPrincipal) { diff --git a/netwerk/base/nsNetUtil.h b/netwerk/base/nsNetUtil.h index bd89ca8aeb..385f2c968e 100644 --- a/netwerk/base/nsNetUtil.h +++ b/netwerk/base/nsNetUtil.h @@ -959,6 +959,11 @@ bool NS_IsReasonableHTTPHeaderValue(const nsACString &aValue); */ bool NS_IsValidHTTPToken(const nsACString &aToken); +/** + * Strip the leading or trailing HTTP whitespace per fetch spec section 2.2. + */ +void NS_TrimHTTPWhitespace(const nsACString& aSource, nsACString& aDest); + /** * Return true if the given request must be upgraded to HTTPS. */ diff --git a/netwerk/protocol/http/nsHttp.cpp b/netwerk/protocol/http/nsHttp.cpp index faf9e21ffd..88022835e3 100644 --- a/netwerk/protocol/http/nsHttp.cpp +++ b/netwerk/protocol/http/nsHttp.cpp @@ -245,6 +245,18 @@ nsHttp::GetProtocolVersion(uint32_t pv) } } +// static +void +nsHttp::TrimHTTPWhitespace(const nsACString& aSource, nsACString& aDest) +{ + nsAutoCString str(aSource); + + // HTTP whitespace 0x09: '\t', 0x0A: '\n', 0x0D: '\r', 0x20: ' ' + static const char kHTTPWhitespace[] = "\t\n\r "; + str.Trim(kHTTPWhitespace); + aDest.Assign(str); +} + // static bool nsHttp::IsReasonableHeaderValue(const nsACString &s) diff --git a/netwerk/protocol/http/nsHttp.h b/netwerk/protocol/http/nsHttp.h index a20637808a..b8fee97079 100644 --- a/netwerk/protocol/http/nsHttp.h +++ b/netwerk/protocol/http/nsHttp.h @@ -148,6 +148,10 @@ struct nsHttp static inline bool IsValidToken(const nsACString &s) { return IsValidToken(s.BeginReading(), s.EndReading()); } + + // Strip the leading or trailing HTTP whitespace per fetch spec section 2.2. + static void TrimHTTPWhitespace(const nsACString& aSource, + nsACString& aDest); // Returns true if the specified value is reasonable given the defintion // in RFC 2616 section 4.2. Full strict validation is not performed