Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 "nsIThreadRetargetableStreamListener.h"
#include "nsString.h"
#include "mozilla/Assertions.h"
#include "mozilla/Components.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.IsPrivateBrowsing());
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, false);
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->GetWebExposedOriginSerialization(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);
}
NS_IMETHODIMP
nsCORSListenerProxy::OnDataFinished(nsresult aStatus) {
nsCOMPtr<nsIStreamListener> listener;
{
MutexAutoLock lock(mMutex);
listener = mOuterListener;
}
if (!listener) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener =
do_QueryInterface(listener);
if (retargetableListener) {
return retargetableListener->OnDataFinished(aStatus);
}
return NS_OK;
}
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, false);
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;
// We need to strip auth header from preflight request for
// cross-origin redirects.
// See Bug 1874132
bool stripAuthHeader =
NS_ShouldRemoveAuthHeaderOnRedirect(aOldChannel, aNewChannel, aFlags);
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,
stripAuthHeader);
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
// 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,
bool aStripAuthHeader) {
MOZ_ASSERT_IF(aUpdateType == UpdateType::InternalOrHSTSRedirect,
!aStripAuthHeader);
nsCOMPtr<nsIURI> uri, originalURI;
nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri));
NS_ENSURE_SUCCESS(rv, rv);
rv = aChannel->GetOriginalURI(getter_AddRefs(originalURI));