diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /netwerk/protocol/http/nsCORSListenerProxy.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'netwerk/protocol/http/nsCORSListenerProxy.cpp')
-rw-r--r-- | netwerk/protocol/http/nsCORSListenerProxy.cpp | 1734 |
1 files changed, 1734 insertions, 0 deletions
diff --git a/netwerk/protocol/http/nsCORSListenerProxy.cpp b/netwerk/protocol/http/nsCORSListenerProxy.cpp new file mode 100644 index 0000000000..9e7775bcfc --- /dev/null +++ b/netwerk/protocol/http/nsCORSListenerProxy.cpp @@ -0,0 +1,1734 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsString.h" +#include "mozilla/Assertions.h" +#include "mozilla/LinkedList.h" +#include "mozilla/StaticPrefs_content.h" +#include "mozilla/StoragePrincipalHelper.h" + +#include "nsCORSListenerProxy.h" +#include "nsIChannel.h" +#include "nsIHttpChannel.h" +#include "HttpChannelChild.h" +#include "nsIHttpChannelInternal.h" +#include "nsError.h" +#include "nsContentUtils.h" +#include "nsNetUtil.h" +#include "nsComponentManagerUtils.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsMimeTypes.h" +#include "nsStringStream.h" +#include "nsGkAtoms.h" +#include "nsWhitespaceTokenizer.h" +#include "nsIChannelEventSink.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsAsyncRedirectVerifyHelper.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" +#include "nsStreamUtils.h" +#include "mozilla/Preferences.h" +#include "nsIScriptError.h" +#include "nsILoadGroup.h" +#include "nsILoadContext.h" +#include "nsIConsoleService.h" +#include "nsINetworkInterceptController.h" +#include "nsICorsPreflightCallback.h" +#include "nsISupportsImpl.h" +#include "nsHttpChannel.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ExpandedPrincipal.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/NullPrincipal.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsQueryObject.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/nsHTTPSOnlyUtils.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/glean/GleanMetrics.h" +#include <algorithm> + +using namespace mozilla; +using namespace mozilla::net; + +#define PREFLIGHT_CACHE_SIZE 100 +// 5 seconds is chosen to be compatible with Chromium. +#define PREFLIGHT_DEFAULT_EXPIRY_SECONDS 5 + +static inline nsAutoString GetStatusCodeAsString(nsIHttpChannel* aHttp) { + nsAutoString result; + uint32_t code; + if (NS_SUCCEEDED(aHttp->GetResponseStatus(&code))) { + result.AppendInt(code); + } + return result; +} + +static void LogBlockedRequest(nsIRequest* aRequest, const char* aProperty, + const char16_t* aParam, uint32_t aBlockingReason, + nsIHttpChannel* aCreatingChannel, + bool aIsWarning = false) { + nsresult rv = NS_OK; + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + + if (!aIsWarning) { + NS_SetRequestBlockingReason(channel, aBlockingReason); + } + + nsCOMPtr<nsIURI> aUri; + channel->GetURI(getter_AddRefs(aUri)); + nsAutoCString spec; + if (aUri) { + spec = aUri->GetSpecOrDefault(); + } + + // Generate the error message + nsAutoString blockedMessage; + AutoTArray<nsString, 2> params; + CopyUTF8toUTF16(spec, *params.AppendElement()); + if (aParam) { + params.AppendElement(aParam); + } + NS_ConvertUTF8toUTF16 specUTF16(spec); + rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eSECURITY_PROPERTIES, aProperty, params, blockedMessage); + + if (NS_FAILED(rv)) { + NS_WARNING("Failed to log blocked cross-site request (no formalizedStr"); + return; + } + + nsAutoString msg(blockedMessage.get()); + nsDependentCString category(aProperty); + + if (XRE_IsParentProcess()) { + if (aCreatingChannel) { + rv = aCreatingChannel->LogBlockedCORSRequest(msg, category, aIsWarning); + if (NS_SUCCEEDED(rv)) { + return; + } + } + NS_WARNING( + "Failed to log blocked cross-site request to web console from " + "parent->child, falling back to browser console"); + } + + bool privateBrowsing = false; + if (aRequest) { + nsCOMPtr<nsILoadGroup> loadGroup; + rv = aRequest->GetLoadGroup(getter_AddRefs(loadGroup)); + NS_ENSURE_SUCCESS_VOID(rv); + privateBrowsing = nsContentUtils::IsInPrivateBrowsing(loadGroup); + } + + bool fromChromeContext = false; + if (channel) { + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + fromChromeContext = loadInfo->TriggeringPrincipal()->IsSystemPrincipal(); + } + + // we are passing aProperty as the category so we can link to the + // appropriate MDN docs depending on the specific error. + uint64_t innerWindowID = nsContentUtils::GetInnerWindowID(aRequest); + // The |innerWindowID| could be 0 if this request is created from script. + // We can always try top level content window id in this case, + // since the window id can lead to current top level window's web console. + if (!innerWindowID) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest); + if (httpChannel) { + Unused << httpChannel->GetTopLevelContentWindowId(&innerWindowID); + } + } + nsCORSListenerProxy::LogBlockedCORSRequest(innerWindowID, privateBrowsing, + fromChromeContext, msg, category, + aIsWarning); +} + +////////////////////////////////////////////////////////////////////////// +// Preflight cache + +class nsPreflightCache { + public: + struct TokenTime { + nsCString token; + TimeStamp expirationTime; + }; + + struct CacheEntry : public LinkedListElement<CacheEntry> { + explicit CacheEntry(nsCString& aKey, bool aPrivateBrowsing) + : mKey(aKey), mPrivateBrowsing(aPrivateBrowsing) { + MOZ_COUNT_CTOR(nsPreflightCache::CacheEntry); + } + + ~CacheEntry() { MOZ_COUNT_DTOR(nsPreflightCache::CacheEntry); } + + void PurgeExpired(TimeStamp now); + bool CheckRequest(const nsCString& aMethod, + const nsTArray<nsCString>& aHeaders); + + nsCString mKey; + bool mPrivateBrowsing{false}; + nsTArray<TokenTime> mMethods; + nsTArray<TokenTime> mHeaders; + }; + + MOZ_COUNTED_DEFAULT_CTOR(nsPreflightCache) + + ~nsPreflightCache() { + Clear(); + MOZ_COUNT_DTOR(nsPreflightCache); + } + + bool Initialize() { return true; } + + CacheEntry* GetEntry(nsIURI* aURI, nsIPrincipal* aPrincipal, + bool aWithCredentials, + const OriginAttributes& aOriginAttributes, bool aCreate); + void RemoveEntries(nsIURI* aURI, nsIPrincipal* aPrincipal, + const OriginAttributes& aOriginAttributes); + void PurgePrivateBrowsingEntries(); + + void Clear(); + + private: + nsClassHashtable<nsCStringHashKey, CacheEntry> mTable; + LinkedList<CacheEntry> mList; +}; + +// Will be initialized in EnsurePreflightCache. +static nsPreflightCache* sPreflightCache = nullptr; + +static bool EnsurePreflightCache() { + if (sPreflightCache) return true; + + UniquePtr<nsPreflightCache> newCache(new nsPreflightCache()); + + if (newCache->Initialize()) { + sPreflightCache = newCache.release(); + return true; + } + + return false; +} + +void nsPreflightCache::PurgePrivateBrowsingEntries() { + for (auto iter = mTable.Iter(); !iter.Done(); iter.Next()) { + auto* entry = iter.UserData(); + if (entry->mPrivateBrowsing) { + // last private browsing window closed, remove preflight cache entries + entry->removeFrom(sPreflightCache->mList); + iter.Remove(); + } + } +} + +void nsPreflightCache::CacheEntry::PurgeExpired(TimeStamp now) { + for (uint32_t i = 0, len = mMethods.Length(); i < len; ++i) { + if (now >= mMethods[i].expirationTime) { + mMethods.UnorderedRemoveElementAt(i); + --i; // Examine the element again, if necessary. + --len; + } + } + for (uint32_t i = 0, len = mHeaders.Length(); i < len; ++i) { + if (now >= mHeaders[i].expirationTime) { + mHeaders.UnorderedRemoveElementAt(i); + --i; // Examine the element again, if necessary. + --len; + } + } +} + +bool nsPreflightCache::CacheEntry::CheckRequest( + const nsCString& aMethod, const nsTArray<nsCString>& aHeaders) { + PurgeExpired(TimeStamp::NowLoRes()); + + if (!aMethod.EqualsLiteral("GET") && !aMethod.EqualsLiteral("POST")) { + struct CheckToken { + bool Equals(const TokenTime& e, const nsCString& method) const { + return e.token.Equals(method); + } + }; + + if (!mMethods.Contains(aMethod, CheckToken())) { + return false; + } + } + + struct CheckHeaderToken { + bool Equals(const TokenTime& e, const nsCString& header) const { + return e.token.Equals(header, nsCaseInsensitiveCStringComparator); + } + } checker; + for (uint32_t i = 0; i < aHeaders.Length(); ++i) { + if (!mHeaders.Contains(aHeaders[i], checker)) { + return false; + } + } + + return true; +} + +nsPreflightCache::CacheEntry* nsPreflightCache::GetEntry( + nsIURI* aURI, nsIPrincipal* aPrincipal, bool aWithCredentials, + const OriginAttributes& aOriginAttributes, bool aCreate) { + nsCString key; + if (NS_FAILED(aPrincipal->GetPrefLightCacheKey(aURI, aWithCredentials, + aOriginAttributes, key))) { + NS_WARNING("Invalid cache key!"); + return nullptr; + } + + CacheEntry* existingEntry = nullptr; + + if (mTable.Get(key, &existingEntry)) { + // Entry already existed so just return it. Also update the LRU list. + + // Move to the head of the list. + existingEntry->removeFrom(mList); + mList.insertFront(existingEntry); + + return existingEntry; + } + + if (!aCreate) { + return nullptr; + } + + // This is a new entry, allocate and insert into the table now so that any + // failures don't cause items to be removed from a full cache. + auto newEntry = + MakeUnique<CacheEntry>(key, aOriginAttributes.mPrivateBrowsingId != 0); + + NS_ASSERTION(mTable.Count() <= PREFLIGHT_CACHE_SIZE, + "Something is borked, too many entries in the cache!"); + + // Now enforce the max count. + if (mTable.Count() == PREFLIGHT_CACHE_SIZE) { + // Try to kick out all the expired entries. + TimeStamp now = TimeStamp::NowLoRes(); + for (auto iter = mTable.Iter(); !iter.Done(); iter.Next()) { + auto* entry = iter.UserData(); + entry->PurgeExpired(now); + + if (entry->mHeaders.IsEmpty() && entry->mMethods.IsEmpty()) { + // Expired, remove from the list as well as the hash table. + entry->removeFrom(sPreflightCache->mList); + iter.Remove(); + } + } + + // If that didn't remove anything then kick out the least recently used + // entry. + if (mTable.Count() == PREFLIGHT_CACHE_SIZE) { + CacheEntry* lruEntry = static_cast<CacheEntry*>(mList.popLast()); + MOZ_ASSERT(lruEntry); + + // This will delete 'lruEntry'. + mTable.Remove(lruEntry->mKey); + + NS_ASSERTION(mTable.Count() == PREFLIGHT_CACHE_SIZE - 1, + "Somehow tried to remove an entry that was never added!"); + } + } + + auto* newEntryWeakRef = mTable.InsertOrUpdate(key, std::move(newEntry)).get(); + mList.insertFront(newEntryWeakRef); + + return newEntryWeakRef; +} + +void nsPreflightCache::RemoveEntries( + nsIURI* aURI, nsIPrincipal* aPrincipal, + const OriginAttributes& aOriginAttributes) { + CacheEntry* entry; + nsCString key; + if (NS_SUCCEEDED(aPrincipal->GetPrefLightCacheKey(aURI, true, + aOriginAttributes, key)) && + mTable.Get(key, &entry)) { + entry->removeFrom(mList); + mTable.Remove(key); + } + + if (NS_SUCCEEDED(aPrincipal->GetPrefLightCacheKey(aURI, false, + aOriginAttributes, key)) && + mTable.Get(key, &entry)) { + entry->removeFrom(mList); + mTable.Remove(key); + } +} + +void nsPreflightCache::Clear() { + mList.clear(); + mTable.Clear(); +} + +////////////////////////////////////////////////////////////////////////// +// nsCORSListenerProxy + +NS_IMPL_ISUPPORTS(nsCORSListenerProxy, nsIStreamListener, nsIRequestObserver, + nsIChannelEventSink, nsIInterfaceRequestor, + nsIThreadRetargetableStreamListener) + +/* static */ +void nsCORSListenerProxy::Shutdown() { + delete sPreflightCache; + sPreflightCache = nullptr; +} + +/* static */ +void nsCORSListenerProxy::ClearCache() { + if (!sPreflightCache) { + return; + } + sPreflightCache->Clear(); +} + +// static +void nsCORSListenerProxy::ClearPrivateBrowsingCache() { + if (!sPreflightCache) { + return; + } + sPreflightCache->PurgePrivateBrowsingEntries(); +} + +// Usually, when using an expanded principal, there's no particularly good +// origin to do the request with. However if the expanded principal only wraps +// one principal, we can use that one instead. +// +// This is needed so that DevTools can still do CORS-enabled requests (since +// DevTools uses a triggering principal expanding the node principal to bypass +// CSP checks, see Element::CreateDevToolsPrincipal(), bug 1604562, and bug +// 1391994). +static nsIPrincipal* GetOriginHeaderPrincipal(nsIPrincipal* aPrincipal) { + while (aPrincipal && aPrincipal->GetIsExpandedPrincipal()) { + auto* ep = BasePrincipal::Cast(aPrincipal)->As<ExpandedPrincipal>(); + if (ep->AllowList().Length() != 1) { + break; + } + aPrincipal = ep->AllowList()[0]; + } + return aPrincipal; +} + +nsCORSListenerProxy::nsCORSListenerProxy(nsIStreamListener* aOuter, + nsIPrincipal* aRequestingPrincipal, + bool aWithCredentials) + : mOuterListener(aOuter), + mRequestingPrincipal(aRequestingPrincipal), + mOriginHeaderPrincipal(GetOriginHeaderPrincipal(aRequestingPrincipal)), + mWithCredentials(aWithCredentials), + mRequestApproved(false), + mHasBeenCrossSite(false), +#ifdef DEBUG + mInited(false), +#endif + mMutex("nsCORSListenerProxy") { +} + +nsresult nsCORSListenerProxy::Init(nsIChannel* aChannel, + DataURIHandling aAllowDataURI) { + aChannel->GetNotificationCallbacks( + getter_AddRefs(mOuterNotificationCallbacks)); + aChannel->SetNotificationCallbacks(this); + + nsresult rv = UpdateChannel(aChannel, aAllowDataURI, UpdateType::Default); + if (NS_FAILED(rv)) { + { + MutexAutoLock lock(mMutex); + mOuterListener = nullptr; + } + mRequestingPrincipal = nullptr; + mOriginHeaderPrincipal = nullptr; + mOuterNotificationCallbacks = nullptr; + mHttpChannel = nullptr; + } +#ifdef DEBUG + mInited = true; +#endif + return rv; +} + +NS_IMETHODIMP +nsCORSListenerProxy::OnStartRequest(nsIRequest* aRequest) { + MOZ_ASSERT(mInited, "nsCORSListenerProxy has not been initialized properly"); + nsresult rv = CheckRequestApproved(aRequest); + mRequestApproved = NS_SUCCEEDED(rv); + if (!mRequestApproved) { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + if (channel) { + nsCOMPtr<nsIURI> uri; + NS_GetFinalChannelURI(channel, getter_AddRefs(uri)); + if (uri) { + OriginAttributes attrs; + StoragePrincipalHelper::GetOriginAttributesForNetworkState(channel, + attrs); + + if (sPreflightCache) { + // OK to use mRequestingPrincipal since preflights never get + // redirected. + sPreflightCache->RemoveEntries(uri, mRequestingPrincipal, attrs); + } else { + nsCOMPtr<nsIHttpChannelChild> httpChannelChild = + do_QueryInterface(channel); + if (httpChannelChild) { + rv = httpChannelChild->RemoveCorsPreflightCacheEntry( + uri, mRequestingPrincipal, attrs); + if (NS_FAILED(rv)) { + // Only warn here to ensure we fall through the request Cancel() + // and outer listener OnStartRequest() calls. + NS_WARNING("Failed to remove CORS preflight cache entry!"); + } + } + } + } + } + + aRequest->Cancel(NS_ERROR_DOM_BAD_URI); + nsCOMPtr<nsIStreamListener> listener; + { + MutexAutoLock lock(mMutex); + listener = mOuterListener; + } + listener->OnStartRequest(aRequest); + + // Reason for NS_ERROR_DOM_BAD_URI already logged in CheckRequestApproved() + return NS_ERROR_DOM_BAD_URI; + } + + nsCOMPtr<nsIStreamListener> listener; + { + MutexAutoLock lock(mMutex); + listener = mOuterListener; + } + return listener->OnStartRequest(aRequest); +} + +namespace { +class CheckOriginHeader final : public nsIHttpHeaderVisitor { + public: + NS_DECL_ISUPPORTS + + CheckOriginHeader() = default; + + NS_IMETHOD + VisitHeader(const nsACString& aHeader, const nsACString& aValue) override { + if (aHeader.EqualsLiteral("Access-Control-Allow-Origin")) { + mHeaderCount++; + } + + if (mHeaderCount > 1) { + return NS_ERROR_DOM_BAD_URI; + } + return NS_OK; + } + + private: + uint32_t mHeaderCount{0}; + + ~CheckOriginHeader() = default; +}; + +NS_IMPL_ISUPPORTS(CheckOriginHeader, nsIHttpHeaderVisitor) +} // namespace + +nsresult nsCORSListenerProxy::CheckRequestApproved(nsIRequest* aRequest) { + // Check if this was actually a cross domain request + if (!mHasBeenCrossSite) { + return NS_OK; + } + nsCOMPtr<nsIHttpChannel> topChannel; + topChannel.swap(mHttpChannel); + + if (StaticPrefs::content_cors_disable()) { + LogBlockedRequest(aRequest, "CORSDisabled", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSDISABLED, topChannel); + return NS_ERROR_DOM_BAD_URI; + } + + // Check if the request failed + nsresult status; + nsresult rv = aRequest->GetStatus(&status); + if (NS_FAILED(rv)) { + LogBlockedRequest(aRequest, "CORSDidNotSucceed2", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSDIDNOTSUCCEED, + topChannel); + return rv; + } + + if (NS_FAILED(status)) { + if (NS_BINDING_ABORTED != status) { + // Don't want to log mere cancellation as an error. + LogBlockedRequest(aRequest, "CORSDidNotSucceed2", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSDIDNOTSUCCEED, + topChannel); + } + return status; + } + + // Test that things worked on a HTTP level + nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aRequest); + if (!http) { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + nsCOMPtr<nsIURI> uri; + NS_GetFinalChannelURI(channel, getter_AddRefs(uri)); + if (uri && uri->SchemeIs("moz-extension")) { + // moz-extension:-URLs do not support CORS, but can universally be read + // if an extension lists the resource in web_accessible_resources. + // Access will be checked in UpdateChannel. + return NS_OK; + } + LogBlockedRequest(aRequest, "CORSRequestNotHttp", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSREQUESTNOTHTTP, + topChannel); + return NS_ERROR_DOM_BAD_URI; + } + + nsCOMPtr<nsILoadInfo> loadInfo = http->LoadInfo(); + if (loadInfo->GetServiceWorkerTaintingSynthesized()) { + // For synthesized responses, we don't need to perform any checks. + // Note: This would be unsafe if we ever changed our behavior to allow + // service workers to intercept CORS preflights. + return NS_OK; + } + + // Check the Access-Control-Allow-Origin header + RefPtr<CheckOriginHeader> visitor = new CheckOriginHeader(); + nsAutoCString allowedOriginHeader; + + // check for duplicate headers + rv = http->VisitOriginalResponseHeaders(visitor); + if (NS_FAILED(rv)) { + LogBlockedRequest( + aRequest, "CORSMultipleAllowOriginNotAllowed", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSMULTIPLEALLOWORIGINNOTALLOWED, + topChannel); + return rv; + } + + rv = http->GetResponseHeader("Access-Control-Allow-Origin"_ns, + allowedOriginHeader); + if (NS_FAILED(rv)) { + auto statusCode = GetStatusCodeAsString(http); + LogBlockedRequest(aRequest, "CORSMissingAllowOrigin2", statusCode.get(), + nsILoadInfo::BLOCKING_REASON_CORSMISSINGALLOWORIGIN, + topChannel); + return rv; + } + + // Bug 1210985 - Explicitly point out the error that the credential is + // not supported if the allowing origin is '*'. Note that this check + // has to be done before the condition + // + // >> if (mWithCredentials || !allowedOriginHeader.EqualsLiteral("*")) + // + // below since "if (A && B)" is included in "if (A || !B)". + // + if (mWithCredentials && allowedOriginHeader.EqualsLiteral("*")) { + LogBlockedRequest(aRequest, "CORSNotSupportingCredentials", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSNOTSUPPORTINGCREDENTIALS, + topChannel); + return NS_ERROR_DOM_BAD_URI; + } + + if (mWithCredentials || !allowedOriginHeader.EqualsLiteral("*")) { + MOZ_ASSERT(!mOriginHeaderPrincipal->GetIsExpandedPrincipal()); + nsAutoCString origin; + mOriginHeaderPrincipal->GetAsciiOrigin(origin); + + if (!allowedOriginHeader.Equals(origin)) { + LogBlockedRequest( + aRequest, "CORSAllowOriginNotMatchingOrigin", + NS_ConvertUTF8toUTF16(allowedOriginHeader).get(), + nsILoadInfo::BLOCKING_REASON_CORSALLOWORIGINNOTMATCHINGORIGIN, + topChannel); + return NS_ERROR_DOM_BAD_URI; + } + } + + // Check Access-Control-Allow-Credentials header + if (mWithCredentials) { + nsAutoCString allowCredentialsHeader; + rv = http->GetResponseHeader("Access-Control-Allow-Credentials"_ns, + allowCredentialsHeader); + + if (!allowCredentialsHeader.EqualsLiteral("true")) { + LogBlockedRequest( + aRequest, "CORSMissingAllowCredentials", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSMISSINGALLOWCREDENTIALS, topChannel); + return NS_ERROR_DOM_BAD_URI; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCORSListenerProxy::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + MOZ_ASSERT(mInited, "nsCORSListenerProxy has not been initialized properly"); + nsCOMPtr<nsIStreamListener> listener; + { + MutexAutoLock lock(mMutex); + listener = std::move(mOuterListener); + } + nsresult rv = listener->OnStopRequest(aRequest, aStatusCode); + mOuterNotificationCallbacks = nullptr; + mHttpChannel = nullptr; + return rv; +} + +NS_IMETHODIMP +nsCORSListenerProxy::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + // NB: This can be called on any thread! But we're guaranteed that it is + // called between OnStartRequest and OnStopRequest, so we don't need to worry + // about races. + + MOZ_ASSERT(mInited, "nsCORSListenerProxy has not been initialized properly"); + if (!mRequestApproved) { + // Reason for NS_ERROR_DOM_BAD_URI already logged in CheckRequestApproved() + return NS_ERROR_DOM_BAD_URI; + } + nsCOMPtr<nsIStreamListener> listener; + { + MutexAutoLock lock(mMutex); + listener = mOuterListener; + } + return listener->OnDataAvailable(aRequest, aInputStream, aOffset, aCount); +} + +void nsCORSListenerProxy::SetInterceptController( + nsINetworkInterceptController* aInterceptController) { + mInterceptController = aInterceptController; +} + +NS_IMETHODIMP +nsCORSListenerProxy::GetInterface(const nsIID& aIID, void** aResult) { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + *aResult = static_cast<nsIChannelEventSink*>(this); + NS_ADDREF_THIS(); + + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsINetworkInterceptController)) && + mInterceptController) { + nsCOMPtr<nsINetworkInterceptController> copy(mInterceptController); + *aResult = copy.forget().take(); + + return NS_OK; + } + + return mOuterNotificationCallbacks + ? mOuterNotificationCallbacks->GetInterface(aIID, aResult) + : NS_ERROR_NO_INTERFACE; +} + +NS_IMETHODIMP +nsCORSListenerProxy::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* aCb) { + nsresult rv; + if (NS_IsInternalSameURIRedirect(aOldChannel, aNewChannel, aFlags) || + NS_IsHSTSUpgradeRedirect(aOldChannel, aNewChannel, aFlags)) { + // Internal redirects still need to be updated in order to maintain + // the correct headers. We use DataURIHandling::Allow, since unallowed + // data URIs should have been blocked before we got to the internal + // redirect. + rv = UpdateChannel(aNewChannel, DataURIHandling::Allow, + UpdateType::InternalOrHSTSRedirect); + if (NS_FAILED(rv)) { + NS_WARNING( + "nsCORSListenerProxy::AsyncOnChannelRedirect: " + "internal redirect UpdateChannel() returned failure"); + aOldChannel->Cancel(rv); + return rv; + } + } else { + mIsRedirect = true; + // A real, external redirect. Perform CORS checking on new URL. + rv = CheckRequestApproved(aOldChannel); + if (NS_FAILED(rv)) { + nsCOMPtr<nsIURI> oldURI; + NS_GetFinalChannelURI(aOldChannel, getter_AddRefs(oldURI)); + if (oldURI) { + OriginAttributes attrs; + StoragePrincipalHelper::GetOriginAttributesForNetworkState(aOldChannel, + attrs); + if (sPreflightCache) { + // OK to use mRequestingPrincipal since preflights never get + // redirected. + sPreflightCache->RemoveEntries(oldURI, mRequestingPrincipal, attrs); + } else { + nsCOMPtr<nsIHttpChannelChild> httpChannelChild = + do_QueryInterface(aOldChannel); + if (httpChannelChild) { + rv = httpChannelChild->RemoveCorsPreflightCacheEntry( + oldURI, mRequestingPrincipal, attrs); + if (NS_FAILED(rv)) { + // Only warn here to ensure we call the channel Cancel() below + NS_WARNING("Failed to remove CORS preflight cache entry!"); + } + } + } + } + aOldChannel->Cancel(NS_ERROR_DOM_BAD_URI); + // Reason for NS_ERROR_DOM_BAD_URI already logged in + // CheckRequestApproved() + return NS_ERROR_DOM_BAD_URI; + } + + if (mHasBeenCrossSite) { + // Once we've been cross-site, cross-origin redirects reset our source + // origin. Note that we need to call GetChannelURIPrincipal() because + // we are looking for the principal that is actually being loaded and not + // the principal that initiated the load. + nsCOMPtr<nsIPrincipal> oldChannelPrincipal; + nsContentUtils::GetSecurityManager()->GetChannelURIPrincipal( + aOldChannel, getter_AddRefs(oldChannelPrincipal)); + nsCOMPtr<nsIPrincipal> newChannelPrincipal; + nsContentUtils::GetSecurityManager()->GetChannelURIPrincipal( + aNewChannel, getter_AddRefs(newChannelPrincipal)); + if (!oldChannelPrincipal || !newChannelPrincipal) { + rv = NS_ERROR_OUT_OF_MEMORY; + } + + if (NS_FAILED(rv)) { + aOldChannel->Cancel(rv); + return rv; + } + + if (!oldChannelPrincipal->Equals(newChannelPrincipal)) { + // Spec says to set our source origin to a unique origin. + mOriginHeaderPrincipal = + NullPrincipal::CreateWithInheritedAttributes(oldChannelPrincipal); + } + } + + bool rewriteToGET = false; + nsCOMPtr<nsIHttpChannel> oldHttpChannel = do_QueryInterface(aOldChannel); + if (oldHttpChannel) { + nsAutoCString method; + Unused << oldHttpChannel->GetRequestMethod(method); + Unused << oldHttpChannel->ShouldStripRequestBodyHeader(method, + &rewriteToGET); + } + + rv = UpdateChannel(aNewChannel, DataURIHandling::Disallow, + rewriteToGET ? UpdateType::StripRequestBodyHeader + : UpdateType::Default); + if (NS_FAILED(rv)) { + NS_WARNING( + "nsCORSListenerProxy::AsyncOnChannelRedirect: " + "UpdateChannel() returned failure"); + aOldChannel->Cancel(rv); + return rv; + } + } + + nsCOMPtr<nsIChannelEventSink> outer = + do_GetInterface(mOuterNotificationCallbacks); + if (outer) { + return outer->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCb); + } + + aCb->OnRedirectVerifyCallback(NS_OK); + + return NS_OK; +} + +NS_IMETHODIMP +nsCORSListenerProxy::CheckListenerChain() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener; + { + MutexAutoLock lock(mMutex); + retargetableListener = do_QueryInterface(mOuterListener); + } + if (!retargetableListener) { + return NS_ERROR_NO_INTERFACE; + } + + return retargetableListener->CheckListenerChain(); +} + +// Please note that the CSP directive 'upgrade-insecure-requests' and the +// HTTPS-Only Mode are relying on the promise that channels get updated from +// http: to https: before the channel fetches any data from the netwerk. Such +// channels should not be blocked by CORS and marked as cross origin requests. +// E.g.: toplevel page: https://www.example.com loads +// xhr: http://www.example.com/foo which gets updated to +// https://www.example.com/foo +// In such a case we should bail out of CORS and rely on the promise that +// nsHttpChannel::Connect() upgrades the request from http to https. +bool CheckInsecureUpgradePreventsCORS(nsIPrincipal* aRequestingPrincipal, + nsIChannel* aChannel) { + nsCOMPtr<nsIURI> channelURI; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI)); + NS_ENSURE_SUCCESS(rv, false); + + // upgrade insecure requests is only applicable to http requests + if (!channelURI->SchemeIs("http")) { + return false; + } + + nsCOMPtr<nsIURI> originalURI; + rv = aChannel->GetOriginalURI(getter_AddRefs(originalURI)); + NS_ENSURE_SUCCESS(rv, false); + + nsAutoCString principalHost, channelHost, origChannelHost; + + // if we can not query a host from the uri, there is nothing to do + if (NS_FAILED(aRequestingPrincipal->GetAsciiHost(principalHost)) || + NS_FAILED(channelURI->GetAsciiHost(channelHost)) || + NS_FAILED(originalURI->GetAsciiHost(origChannelHost))) { + return false; + } + + // if the hosts do not match, there is nothing to do + if (!principalHost.EqualsIgnoreCase(channelHost.get())) { + return false; + } + + // also check that uri matches the one of the originalURI + if (!channelHost.EqualsIgnoreCase(origChannelHost.get())) { + return false; + } + + return true; +} + +nsresult nsCORSListenerProxy::UpdateChannel(nsIChannel* aChannel, + DataURIHandling aAllowDataURI, + UpdateType aUpdateType) { + nsCOMPtr<nsIURI> uri, originalURI; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + rv = aChannel->GetOriginalURI(getter_AddRefs(originalURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + // Introduced for DevTools in order to allow overriding some requests + // with the content of data: URIs. + if (loadInfo->GetAllowInsecureRedirectToDataURI() && uri->SchemeIs("data")) { + return NS_OK; + } + + // exempt data URIs from the same origin check. + if (aAllowDataURI == DataURIHandling::Allow && originalURI == uri) { + if (uri->SchemeIs("data")) { + return NS_OK; + } + if (loadInfo->GetAboutBlankInherits() && NS_IsAboutBlank(uri)) { + return NS_OK; + } + } + + // Set CORS attributes on channel so that intercepted requests get correct + // values. We have to do this here because the CheckMayLoad checks may lead + // to early return. We can't be sure this is an http channel though, so we + // can't return early on failure. + nsCOMPtr<nsIHttpChannelInternal> internal = do_QueryInterface(aChannel); + if (internal) { + rv = internal->SetRequestMode(dom::RequestMode::Cors); + NS_ENSURE_SUCCESS(rv, rv); + rv = internal->SetCorsIncludeCredentials(mWithCredentials); + NS_ENSURE_SUCCESS(rv, rv); + } + + // TODO: Bug 1353683 + // consider calling SetBlockedRequest in nsCORSListenerProxy::UpdateChannel + // + // Check that the uri is ok to load + uint32_t flags = loadInfo->CheckLoadURIFlags(); + rv = nsContentUtils::GetSecurityManager()->CheckLoadURIWithPrincipal( + mRequestingPrincipal, uri, flags, loadInfo->GetInnerWindowID()); + NS_ENSURE_SUCCESS(rv, rv); + + if (originalURI != uri) { + rv = nsContentUtils::GetSecurityManager()->CheckLoadURIWithPrincipal( + mRequestingPrincipal, originalURI, flags, loadInfo->GetInnerWindowID()); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (uri->SchemeIs("moz-extension")) { + // moz-extension:-URLs do not support CORS, but can universally be read + // if an extension lists the resource in web_accessible_resources. + // This is enforced via the CheckLoadURIWithPrincipal call above: + // moz-extension resources have the URI_DANGEROUS_TO_LOAD flag, unless + // listed in web_accessible_resources. + return NS_OK; + } + + if (!mHasBeenCrossSite && + NS_SUCCEEDED(mRequestingPrincipal->CheckMayLoad(uri, false)) && + (originalURI == uri || + NS_SUCCEEDED(mRequestingPrincipal->CheckMayLoad(originalURI, false)))) { + return NS_OK; + } + + // If the CSP directive 'upgrade-insecure-requests' is used or the HTTPS-Only + // Mode is enabled then we should not incorrectly require CORS if the only + // difference of a subresource request and the main page is the scheme. e.g. + // toplevel page: https://www.example.com loads + // xhr: http://www.example.com/somefoo, + // then the xhr request will be upgraded to https before it fetches any data + // from the netwerk, hence we shouldn't require CORS in that specific case. + if (CheckInsecureUpgradePreventsCORS(mRequestingPrincipal, aChannel)) { + // Check if https-only mode upgrades this later anyway + nsCOMPtr<nsILoadInfo> loadinfo = aChannel->LoadInfo(); + if (nsHTTPSOnlyUtils::IsSafeToAcceptCORSOrMixedContent(loadinfo)) { + return NS_OK; + } + // Check if 'upgrade-insecure-requests' is used + if (loadInfo->GetUpgradeInsecureRequests() || + loadInfo->GetBrowserUpgradeInsecureRequests()) { + return NS_OK; + } + } + + // Check if we need to do a preflight, and if so set one up. This must be + // called once we know that the request is going, or has gone, cross-origin. + rv = CheckPreflightNeeded(aChannel, aUpdateType); + NS_ENSURE_SUCCESS(rv, rv); + + // It's a cross site load + mHasBeenCrossSite = true; + + if (mIsRedirect || StaticPrefs::network_cors_preflight_block_userpass_uri()) { + // https://fetch.spec.whatwg.org/#http-redirect-fetch + // Step 9. If request’s mode is "cors", locationURL includes credentials, + // and request’s origin is not same origin with locationURL’s origin, + // then return a network error. + + nsAutoCString userpass; + uri->GetUserPass(userpass); + NS_ENSURE_TRUE(userpass.IsEmpty(), NS_ERROR_DOM_BAD_URI); + } + + // If we have an expanded principal here, we'll reject the CORS request, + // because we can't send a useful Origin header which is required for CORS. + if (nsContentUtils::IsExpandedPrincipal(mOriginHeaderPrincipal)) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + LogBlockedRequest(aChannel, "CORSOriginHeaderNotAdded", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSORIGINHEADERNOTADDED, + httpChannel); + return NS_ERROR_DOM_BAD_URI; + } + + // Add the Origin header + nsAutoCString origin; + rv = mOriginHeaderPrincipal->GetAsciiOrigin(origin); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aChannel); + NS_ENSURE_TRUE(http, NS_ERROR_FAILURE); + + // hide the Origin header when requesting from .onion and requesting CORS + if (StaticPrefs::network_http_referer_hideOnionSource()) { + if (mOriginHeaderPrincipal->GetIsOnion()) { + origin.AssignLiteral("null"); + } + } + + rv = http->SetRequestHeader(nsDependentCString(net::nsHttp::Origin), origin, + false); + NS_ENSURE_SUCCESS(rv, rv); + + // Make cookie-less if needed. We don't need to do anything here if the + // channel was opened with AsyncOpen, since then AsyncOpen will take + // care of the cookie policy for us. + if (!mWithCredentials) { + nsLoadFlags flags; + rv = http->GetLoadFlags(&flags); + NS_ENSURE_SUCCESS(rv, rv); + + flags |= nsIRequest::LOAD_ANONYMOUS; + if (StaticPrefs::network_cors_preflight_allow_client_cert()) { + flags |= nsIRequest::LOAD_ANONYMOUS_ALLOW_CLIENT_CERT; + } + rv = http->SetLoadFlags(flags); + NS_ENSURE_SUCCESS(rv, rv); + } + + mHttpChannel = http; + + return NS_OK; +} + +nsresult nsCORSListenerProxy::CheckPreflightNeeded(nsIChannel* aChannel, + UpdateType aUpdateType) { + // If this caller isn't using AsyncOpen, or if this *is* a preflight channel, + // then we shouldn't initiate preflight for this channel. + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + if (loadInfo->GetSecurityMode() != + nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT || + loadInfo->GetIsPreflight()) { + return NS_OK; + } + + bool doPreflight = loadInfo->GetForcePreflight(); + + nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aChannel); + if (!http) { + // Note: A preflight is not needed for moz-extension:-requests either, but + // there is already a check for that in the caller of CheckPreflightNeeded, + // in UpdateChannel. + LogBlockedRequest(aChannel, "CORSRequestNotHttp", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSREQUESTNOTHTTP, + mHttpChannel); + return NS_ERROR_DOM_BAD_URI; + } + + nsAutoCString method; + Unused << http->GetRequestMethod(method); + if (!method.LowerCaseEqualsLiteral("get") && + !method.LowerCaseEqualsLiteral("post") && + !method.LowerCaseEqualsLiteral("head")) { + doPreflight = true; + } + + // Avoid copying the array here + const nsTArray<nsCString>& loadInfoHeaders = loadInfo->CorsUnsafeHeaders(); + if (!loadInfoHeaders.IsEmpty()) { + doPreflight = true; + } + + // Add Content-Type header if needed + nsTArray<nsCString> headers; + nsAutoCString contentTypeHeader; + nsresult rv = http->GetRequestHeader("Content-Type"_ns, contentTypeHeader); + // GetRequestHeader return an error if the header is not set. Don't add + // "content-type" to the list if that's the case. + if (NS_SUCCEEDED(rv) && + !nsContentUtils::IsAllowedNonCorsContentType(contentTypeHeader) && + !loadInfoHeaders.Contains("content-type"_ns, + nsCaseInsensitiveCStringArrayComparator())) { + headers.AppendElements(loadInfoHeaders); + headers.AppendElement("content-type"_ns); + doPreflight = true; + } + + if (!doPreflight) { + return NS_OK; + } + + nsCOMPtr<nsIHttpChannelInternal> internal = do_QueryInterface(http); + if (!internal) { + auto statusCode = GetStatusCodeAsString(http); + LogBlockedRequest(aChannel, "CORSDidNotSucceed2", statusCode.get(), + nsILoadInfo::BLOCKING_REASON_CORSDIDNOTSUCCEED, + mHttpChannel); + return NS_ERROR_DOM_BAD_URI; + } + + internal->SetCorsPreflightParameters( + headers.IsEmpty() ? loadInfoHeaders : headers, + aUpdateType == UpdateType::StripRequestBodyHeader); + + return NS_OK; +} + +////////////////////////////////////////////////////////////////////////// +// Preflight proxy + +// Class used as streamlistener and notification callback when +// doing the initial OPTIONS request for a CORS check +class nsCORSPreflightListener final : public nsIStreamListener, + public nsIInterfaceRequestor, + public nsIChannelEventSink { + public: + nsCORSPreflightListener(nsIPrincipal* aReferrerPrincipal, + nsICorsPreflightCallback* aCallback, + nsILoadContext* aLoadContext, bool aWithCredentials, + const nsCString& aPreflightMethod, + const nsTArray<nsCString>& aPreflightHeaders) + : mPreflightMethod(aPreflightMethod), + mPreflightHeaders(aPreflightHeaders.Clone()), + mReferrerPrincipal(aReferrerPrincipal), + mCallback(aCallback), + mLoadContext(aLoadContext), + mWithCredentials(aWithCredentials) {} + + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + + nsresult CheckPreflightRequestApproved(nsIRequest* aRequest); + + private: + ~nsCORSPreflightListener() = default; + + void AddResultToCache(nsIRequest* aRequest); + + nsCString mPreflightMethod; + nsTArray<nsCString> mPreflightHeaders; + nsCOMPtr<nsIPrincipal> mReferrerPrincipal; + nsCOMPtr<nsICorsPreflightCallback> mCallback; + nsCOMPtr<nsILoadContext> mLoadContext; + bool mWithCredentials; +}; + +NS_IMPL_ISUPPORTS(nsCORSPreflightListener, nsIStreamListener, + nsIRequestObserver, nsIInterfaceRequestor, + nsIChannelEventSink) + +void nsCORSPreflightListener::AddResultToCache(nsIRequest* aRequest) { + nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aRequest); + NS_ASSERTION(http, "Request was not http"); + + // The "Access-Control-Max-Age" header should return an age in seconds. + nsAutoCString headerVal; + uint32_t age = 0; + Unused << http->GetResponseHeader("Access-Control-Max-Age"_ns, headerVal); + if (headerVal.IsEmpty()) { + age = PREFLIGHT_DEFAULT_EXPIRY_SECONDS; + } else { + // Sanitize the string. We only allow 'delta-seconds' as specified by + // http://dev.w3.org/2006/waf/access-control (digits 0-9 with no leading or + // trailing non-whitespace characters). + nsACString::const_char_iterator iter, end; + headerVal.BeginReading(iter); + headerVal.EndReading(end); + while (iter != end) { + if (*iter < '0' || *iter > '9') { + return; + } + age = age * 10 + (*iter - '0'); + // Cap at 24 hours. This also avoids overflow + age = std::min(age, 86400U); + ++iter; + } + } + + if (!age || !EnsurePreflightCache()) { + return; + } + + // String seems fine, go ahead and cache. + // Note that we have already checked that these headers follow the correct + // syntax. + + nsCOMPtr<nsIURI> uri; + NS_GetFinalChannelURI(http, getter_AddRefs(uri)); + + TimeStamp expirationTime = + TimeStamp::NowLoRes() + TimeDuration::FromSeconds(age); + + OriginAttributes attrs; + StoragePrincipalHelper::GetOriginAttributesForNetworkState(http, attrs); + + nsPreflightCache::CacheEntry* entry = sPreflightCache->GetEntry( + uri, mReferrerPrincipal, mWithCredentials, attrs, true); + if (!entry) { + return; + } + + // The "Access-Control-Allow-Methods" header contains a comma separated + // list of method names. + Unused << http->GetResponseHeader("Access-Control-Allow-Methods"_ns, + headerVal); + + for (const nsACString& method : + nsCCharSeparatedTokenizer(headerVal, ',').ToRange()) { + if (method.IsEmpty()) { + continue; + } + uint32_t i; + for (i = 0; i < entry->mMethods.Length(); ++i) { + if (entry->mMethods[i].token.Equals(method)) { + entry->mMethods[i].expirationTime = expirationTime; + break; + } + } + if (i == entry->mMethods.Length()) { + nsPreflightCache::TokenTime* newMethod = entry->mMethods.AppendElement(); + if (!newMethod) { + return; + } + + newMethod->token = method; + newMethod->expirationTime = expirationTime; + } + } + + // The "Access-Control-Allow-Headers" header contains a comma separated + // list of method names. + Unused << http->GetResponseHeader("Access-Control-Allow-Headers"_ns, + headerVal); + + for (const nsACString& header : + nsCCharSeparatedTokenizer(headerVal, ',').ToRange()) { + if (header.IsEmpty()) { + continue; + } + uint32_t i; + for (i = 0; i < entry->mHeaders.Length(); ++i) { + if (entry->mHeaders[i].token.Equals(header)) { + entry->mHeaders[i].expirationTime = expirationTime; + break; + } + } + if (i == entry->mHeaders.Length()) { + nsPreflightCache::TokenTime* newHeader = entry->mHeaders.AppendElement(); + if (!newHeader) { + return; + } + + newHeader->token = header; + newHeader->expirationTime = expirationTime; + } + } +} + +NS_IMETHODIMP +nsCORSPreflightListener::OnStartRequest(nsIRequest* aRequest) { +#ifdef DEBUG + { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + nsCOMPtr<nsILoadInfo> loadInfo = channel ? channel->LoadInfo() : nullptr; + MOZ_ASSERT(!loadInfo || !loadInfo->GetServiceWorkerTaintingSynthesized()); + } +#endif + + nsresult rv = CheckPreflightRequestApproved(aRequest); + + if (NS_SUCCEEDED(rv)) { + // Everything worked, try to cache and then fire off the actual request. + AddResultToCache(aRequest); + + mCallback->OnPreflightSucceeded(); + } else { + mCallback->OnPreflightFailed(rv); + } + + return rv; +} + +NS_IMETHODIMP +nsCORSPreflightListener::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) { + mCallback = nullptr; + return NS_OK; +} + +/** nsIStreamListener methods **/ + +NS_IMETHODIMP +nsCORSPreflightListener::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* inStr, + uint64_t sourceOffset, + uint32_t count) { + uint32_t totalRead; + return inStr->ReadSegments(NS_DiscardSegment, nullptr, count, &totalRead); +} + +NS_IMETHODIMP +nsCORSPreflightListener::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* callback) { + // Only internal redirects allowed for now. + if (!NS_IsInternalSameURIRedirect(aOldChannel, aNewChannel, aFlags) && + !NS_IsHSTSUpgradeRedirect(aOldChannel, aNewChannel, aFlags)) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aOldChannel); + LogBlockedRequest( + aOldChannel, "CORSExternalRedirectNotAllowed", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSEXTERNALREDIRECTNOTALLOWED, + httpChannel); + return NS_ERROR_DOM_BAD_URI; + } + + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +nsresult nsCORSPreflightListener::CheckPreflightRequestApproved( + nsIRequest* aRequest) { + nsresult status; + nsresult rv = aRequest->GetStatus(&status); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_SUCCESS(status, status); + + // Test that things worked on a HTTP level + nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aRequest); + nsCOMPtr<nsIHttpChannelInternal> internal = do_QueryInterface(aRequest); + NS_ENSURE_STATE(internal); + nsCOMPtr<nsIHttpChannel> parentHttpChannel = do_QueryInterface(mCallback); + + bool succeedded; + rv = http->GetRequestSucceeded(&succeedded); + if (NS_FAILED(rv) || !succeedded) { + auto statusCode = GetStatusCodeAsString(http); + LogBlockedRequest(aRequest, "CORSPreflightDidNotSucceed3", statusCode.get(), + nsILoadInfo::BLOCKING_REASON_CORSPREFLIGHTDIDNOTSUCCEED, + parentHttpChannel); + return NS_ERROR_DOM_BAD_URI; + } + + nsAutoCString headerVal; + // The "Access-Control-Allow-Methods" header contains a comma separated + // list of method names. + Unused << http->GetResponseHeader("Access-Control-Allow-Methods"_ns, + headerVal); + bool foundMethod = mPreflightMethod.EqualsLiteral("GET") || + mPreflightMethod.EqualsLiteral("HEAD") || + mPreflightMethod.EqualsLiteral("POST"); + for (const nsACString& method : + nsCCharSeparatedTokenizer(headerVal, ',').ToRange()) { + if (method.IsEmpty()) { + continue; + } + if (!NS_IsValidHTTPToken(method)) { + LogBlockedRequest(aRequest, "CORSInvalidAllowMethod", + NS_ConvertUTF8toUTF16(method).get(), + nsILoadInfo::BLOCKING_REASON_CORSINVALIDALLOWMETHOD, + parentHttpChannel); + return NS_ERROR_DOM_BAD_URI; + } + + if (method.EqualsLiteral("*") && !mWithCredentials) { + foundMethod = true; + } else { + foundMethod |= mPreflightMethod.Equals(method); + } + } + if (!foundMethod) { + LogBlockedRequest(aRequest, "CORSMethodNotFound", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSMETHODNOTFOUND, + parentHttpChannel); + return NS_ERROR_DOM_BAD_URI; + } + + // The "Access-Control-Allow-Headers" header contains a comma separated + // list of header names. + Unused << http->GetResponseHeader("Access-Control-Allow-Headers"_ns, + headerVal); + nsTArray<nsCString> headers; + bool wildcard = false; + bool hasAuthorizationHeader = false; + for (const nsACString& header : + nsCCharSeparatedTokenizer(headerVal, ',').ToRange()) { + if (header.IsEmpty()) { + continue; + } + if (!NS_IsValidHTTPToken(header)) { + LogBlockedRequest(aRequest, "CORSInvalidAllowHeader", + NS_ConvertUTF8toUTF16(header).get(), + nsILoadInfo::BLOCKING_REASON_CORSINVALIDALLOWHEADER, + parentHttpChannel); + return NS_ERROR_DOM_BAD_URI; + } + if (header.EqualsLiteral("*") && !mWithCredentials) { + wildcard = true; + } else { + headers.AppendElement(header); + } + + if (header.LowerCaseEqualsASCII("authorization")) { + hasAuthorizationHeader = true; + } + } + + bool authorizationInPreflightHeaders = false; + bool authorizationCoveredByWildcard = false; + for (uint32_t i = 0; i < mPreflightHeaders.Length(); ++i) { + // Cache the result of the authorization header. + bool isAuthorization = + mPreflightHeaders[i].LowerCaseEqualsASCII("authorization"); + if (wildcard) { + if (!isAuthorization) { + continue; + } else { + authorizationInPreflightHeaders = true; + if (StaticPrefs:: + network_cors_preflight_authorization_covered_by_wildcard() && + !hasAuthorizationHeader) { + // When `Access-Control-Allow-Headers` is `*` and there is no + // `Authorization` header listed, we send a deprecation warning to the + // console. + LogBlockedRequest(aRequest, "CORSAllowHeaderFromPreflightDeprecation", + nullptr, 0, parentHttpChannel, true); + glean::network::cors_authorization_header + .Get("covered_by_wildcard"_ns) + .Add(1); + authorizationCoveredByWildcard = true; + continue; + } + } + } + + const auto& comparator = nsCaseInsensitiveCStringArrayComparator(); + if (!headers.Contains(mPreflightHeaders[i], comparator)) { + LogBlockedRequest( + aRequest, "CORSMissingAllowHeaderFromPreflight2", + NS_ConvertUTF8toUTF16(mPreflightHeaders[i]).get(), + nsILoadInfo::BLOCKING_REASON_CORSMISSINGALLOWHEADERFROMPREFLIGHT, + parentHttpChannel); + if (isAuthorization) { + glean::network::cors_authorization_header.Get("disallowed"_ns).Add(1); + } + return NS_ERROR_DOM_BAD_URI; + } + } + + if (authorizationInPreflightHeaders && !authorizationCoveredByWildcard) { + glean::network::cors_authorization_header.Get("allowed"_ns).Add(1); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCORSPreflightListener::GetInterface(const nsIID& aIID, void** aResult) { + if (aIID.Equals(NS_GET_IID(nsILoadContext)) && mLoadContext) { + nsCOMPtr<nsILoadContext> copy = mLoadContext; + copy.forget(aResult); + return NS_OK; + } + + return QueryInterface(aIID, aResult); +} + +void nsCORSListenerProxy::RemoveFromCorsPreflightCache( + nsIURI* aURI, nsIPrincipal* aRequestingPrincipal, + const OriginAttributes& aOriginAttributes) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (sPreflightCache) { + sPreflightCache->RemoveEntries(aURI, aRequestingPrincipal, + aOriginAttributes); + } +} + +// static +nsresult nsCORSListenerProxy::StartCORSPreflight( + nsIChannel* aRequestChannel, nsICorsPreflightCallback* aCallback, + nsTArray<nsCString>& aUnsafeHeaders, nsIChannel** aPreflightChannel) { + *aPreflightChannel = nullptr; + + if (StaticPrefs::content_cors_disable()) { + nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aRequestChannel); + LogBlockedRequest(aRequestChannel, "CORSDisabled", nullptr, + nsILoadInfo::BLOCKING_REASON_CORSDISABLED, http); + return NS_ERROR_DOM_BAD_URI; + } + + nsAutoCString method; + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aRequestChannel)); + NS_ENSURE_TRUE(httpChannel, NS_ERROR_UNEXPECTED); + Unused << httpChannel->GetRequestMethod(method); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_GetFinalChannelURI(aRequestChannel, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILoadInfo> originalLoadInfo = aRequestChannel->LoadInfo(); + MOZ_ASSERT(originalLoadInfo->GetSecurityMode() == + nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT, + "how did we end up here?"); + + nsCOMPtr<nsIPrincipal> principal = originalLoadInfo->GetLoadingPrincipal(); + MOZ_ASSERT(principal && originalLoadInfo->GetExternalContentPolicyType() != + ExtContentPolicy::TYPE_DOCUMENT, + "Should not do CORS loads for top-level loads, so a " + "loadingPrincipal should always exist."); + bool withCredentials = + originalLoadInfo->GetCookiePolicy() == nsILoadInfo::SEC_COOKIES_INCLUDE; + + nsPreflightCache::CacheEntry* entry = nullptr; + + // Disable cache if devtools says so. + bool disableCache = Preferences::GetBool("devtools.cache.disabled"); + + if (sPreflightCache && !disableCache) { + OriginAttributes attrs; + StoragePrincipalHelper::GetOriginAttributesForNetworkState(aRequestChannel, + attrs); + entry = sPreflightCache->GetEntry(uri, principal, withCredentials, attrs, + false); + } + + if (entry && entry->CheckRequest(method, aUnsafeHeaders)) { + aCallback->OnPreflightSucceeded(); + return NS_OK; + } + + // Either it wasn't cached or the cached result has expired. Build a + // channel for the OPTIONS request. + + nsCOMPtr<nsILoadInfo> loadInfo = + static_cast<mozilla::net::LoadInfo*>(originalLoadInfo.get()) + ->CloneForNewRequest(); + static_cast<mozilla::net::LoadInfo*>(loadInfo.get())->SetIsPreflight(); + + nsCOMPtr<nsILoadGroup> loadGroup; + rv = aRequestChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + NS_ENSURE_SUCCESS(rv, rv); + + // We want to give the preflight channel's notification callbacks the same + // load context as the original channel's notification callbacks had. We + // don't worry about a load context provided via the loadgroup here, since + // they have the same loadgroup. + nsCOMPtr<nsIInterfaceRequestor> callbacks; + rv = aRequestChannel->GetNotificationCallbacks(getter_AddRefs(callbacks)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsILoadContext> loadContext = do_GetInterface(callbacks); + + nsLoadFlags loadFlags; + rv = aRequestChannel->GetLoadFlags(&loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + // Preflight requests should never be intercepted by service workers and + // are always anonymous. + // NOTE: We ignore CORS checks on synthesized responses (see the CORS + // preflights, then we need to extend the GetResponseSynthesized() check in + // nsCORSListenerProxy::CheckRequestApproved()). If we change our behavior + // here and allow service workers to intercept CORS preflights, then that + // check won't be safe any more. + loadFlags |= + nsIChannel::LOAD_BYPASS_SERVICE_WORKER | nsIRequest::LOAD_ANONYMOUS; + + if (StaticPrefs::network_cors_preflight_allow_client_cert()) { + loadFlags |= nsIRequest::LOAD_ANONYMOUS_ALLOW_CLIENT_CERT; + } + + nsCOMPtr<nsIChannel> preflightChannel; + rv = NS_NewChannelInternal(getter_AddRefs(preflightChannel), uri, loadInfo, + nullptr, // PerformanceStorage + loadGroup, + nullptr, // aCallbacks + loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + // Set method and headers + nsCOMPtr<nsIHttpChannel> preHttp = do_QueryInterface(preflightChannel); + NS_ASSERTION(preHttp, "Failed to QI to nsIHttpChannel!"); + + rv = preHttp->SetRequestMethod("OPTIONS"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + rv = preHttp->SetRequestHeader("Access-Control-Request-Method"_ns, method, + false); + NS_ENSURE_SUCCESS(rv, rv); + + // Set the CORS preflight channel's warning reporter to be the same as the + // requesting channel so that all log messages are able to be reported through + // the warning reporter. + RefPtr<nsHttpChannel> reqCh = do_QueryObject(aRequestChannel); + RefPtr<nsHttpChannel> preCh = do_QueryObject(preHttp); + if (preCh && reqCh) { // there are other implementers of nsIHttpChannel + preCh->SetWarningReporter(reqCh->GetWarningReporter()); + } + + nsTArray<nsCString> preflightHeaders; + if (!aUnsafeHeaders.IsEmpty()) { + for (uint32_t i = 0; i < aUnsafeHeaders.Length(); ++i) { + preflightHeaders.AppendElement(); + ToLowerCase(aUnsafeHeaders[i], preflightHeaders[i]); + } + preflightHeaders.Sort(); + nsAutoCString headers; + for (uint32_t i = 0; i < preflightHeaders.Length(); ++i) { + if (i != 0) { + headers += ','; + } + headers += preflightHeaders[i]; + } + rv = preHttp->SetRequestHeader("Access-Control-Request-Headers"_ns, headers, + false); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Set up listener which will start the original channel + RefPtr<nsCORSPreflightListener> preflightListener = + new nsCORSPreflightListener(principal, aCallback, loadContext, + withCredentials, method, preflightHeaders); + + rv = preflightChannel->SetNotificationCallbacks(preflightListener); + NS_ENSURE_SUCCESS(rv, rv); + + if (preCh && reqCh) { + // Per https://fetch.spec.whatwg.org/#cors-preflight-fetch step 1, the + // request's referrer and referrer policy should match the original request. + nsCOMPtr<nsIReferrerInfo> referrerInfo; + rv = reqCh->GetReferrerInfo(getter_AddRefs(referrerInfo)); + NS_ENSURE_SUCCESS(rv, rv); + if (referrerInfo) { + nsCOMPtr<nsIReferrerInfo> newReferrerInfo = + static_cast<dom::ReferrerInfo*>(referrerInfo.get())->Clone(); + rv = preCh->SetReferrerInfo(newReferrerInfo); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Start preflight + rv = preflightChannel->AsyncOpen(preflightListener); + NS_ENSURE_SUCCESS(rv, rv); + + // Return newly created preflight channel + preflightChannel.forget(aPreflightChannel); + + return NS_OK; +} + +// static +void nsCORSListenerProxy::LogBlockedCORSRequest( + uint64_t aInnerWindowID, bool aPrivateBrowsing, bool aFromChromeContext, + const nsAString& aMessage, const nsACString& aCategory, bool aIsWarning) { + nsresult rv = NS_OK; + + // Build the error object and log it to the console + nsCOMPtr<nsIConsoleService> console( + do_GetService(NS_CONSOLESERVICE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to log blocked cross-site request (no console)"); + return; + } + + nsCOMPtr<nsIScriptError> scriptError = + do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to log blocked cross-site request (no scriptError)"); + return; + } + + uint32_t errorFlag = + aIsWarning ? nsIScriptError::warningFlag : nsIScriptError::errorFlag; + + // query innerWindowID and log to web console, otherwise log to + // the error to the browser console. + if (aInnerWindowID > 0) { + rv = scriptError->InitWithSanitizedSource(aMessage, + u""_ns, // sourceName + u""_ns, // sourceLine + 0, // lineNumber + 0, // columnNumber + errorFlag, aCategory, + aInnerWindowID); + } else { + rv = scriptError->Init(aMessage, + u""_ns, // sourceName + u""_ns, // sourceLine + 0, // lineNumber + 0, // columnNumber + errorFlag, aCategory, aPrivateBrowsing, + aFromChromeContext); // From chrome context + } + if (NS_FAILED(rv)) { + NS_WARNING( + "Failed to log blocked cross-site request (scriptError init failed)"); + return; + } + console->LogMessage(scriptError); +} |