[Network, DOM] Align our implementation with the current CORS/Fetch spec.

This commit is contained in:
Moonchild 2022-11-18 20:00:59 +00:00 • committed by roytam1
commit 37f0199c79
9 changed files with 366 additions and 31 deletions

View file

@ -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()
{

View file

@ -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,
*/

View file

@ -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<HeadersEntry>& 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;

View file

@ -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);

View file

@ -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() };

View file

@ -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)
{

View file

@ -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.
*/

View file

@ -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)

View file

@ -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