mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-25 11:58:55 +00:00
Bug 1823877 - Part 1: Filter opaque results from fetch() in the parent for ORB. r=sefeng,smaug,necko-reviewers,edenchuang,valentin
We make sure to not send any data to the content process in case of fetching an opaque resource. This is way to remain more web compatible, but is also in conflict with the ORB specification. Differential Revision: https://phabricator.services.mozilla.com/D173454
This commit is contained in:
parent
f317ffb43d
commit
476835e1c7
@ -1737,6 +1737,29 @@
|
||||
value: @IS_EARLY_BETA_OR_EARLIER@
|
||||
mirror: always
|
||||
|
||||
# This pref controls how filtering of opaque responses for calls to `Window.fetch`.
|
||||
# (and similar) is performed in the parent process. This is intended to make sure
|
||||
# that data that would be filtered in a content process never actually reaches that
|
||||
# content process.
|
||||
# See https://fetch.spec.whatwg.org/#concept-filtered-response-opaque
|
||||
# 0) Don't filter in the parent process at all, and let content processes handle
|
||||
# opaque filtering. Regardless of if ORB is enabled or not. N.B. that if ORB
|
||||
# is enabled opaque responses will be blocked.
|
||||
# 1) If ORB is enabled, in the parent process, filter the responses that ORB allows.
|
||||
# N.B. any responses ORB doesn't allow will not send data to a content process
|
||||
# since they will return a NetworkError. If the request is allowed by ORB, the
|
||||
# internal response will be intact and sent to the content process as is.
|
||||
# 2) If ORB is enabled, in the parent process, filter the responses that ORB blocks,
|
||||
# if they were issued by `Window.fetch` (and similar).
|
||||
# 3) Filter all responses in the parent, regardless of if ORB is enabled or not.
|
||||
# This means that opaque responses coming from `Window.fetch` won't even be
|
||||
# considered for being blocked by ORB.
|
||||
- name: browser.opaqueResponseBlocking.filterFetchResponse
|
||||
type: uint32_t
|
||||
value: 2
|
||||
mirror: always
|
||||
do_not_use_directly: true
|
||||
|
||||
# When this pref is enabled, <object> and <embed> elements will create
|
||||
# synthetic documents when the resource type they're loading is an image.
|
||||
- name: browser.opaqueResponseBlocking.syntheticBrowsingContext
|
||||
|
@ -52,6 +52,7 @@
|
||||
#include "nsContentSecurityManager.h"
|
||||
#include "nsContentSecurityUtils.h"
|
||||
#include "nsContentUtils.h"
|
||||
#include "nsDebug.h"
|
||||
#include "nsEscape.h"
|
||||
#include "nsGlobalWindowOuter.h"
|
||||
#include "nsHttpChannel.h"
|
||||
@ -104,9 +105,9 @@
|
||||
#include "nsQueryObject.h"
|
||||
|
||||
using mozilla::dom::RequestMode;
|
||||
extern mozilla::LazyLogModule gORBLog;
|
||||
#define LOGORB(msg, ...) \
|
||||
MOZ_LOG(gORBLog, LogLevel::Debug, \
|
||||
|
||||
#define LOGORB(msg, ...) \
|
||||
MOZ_LOG(GetORBLog(), LogLevel::Debug, \
|
||||
("%s: %p " msg, __func__, this, ##__VA_ARGS__))
|
||||
|
||||
namespace mozilla {
|
||||
@ -184,6 +185,17 @@ class AddHeadersToChannelVisitor final : public nsIHttpHeaderVisitor {
|
||||
|
||||
NS_IMPL_ISUPPORTS(AddHeadersToChannelVisitor, nsIHttpHeaderVisitor)
|
||||
|
||||
static OpaqueResponseFilterFetch ConfiguredFilterFetchResponseBehaviour() {
|
||||
uint32_t pref = StaticPrefs::
|
||||
browser_opaqueResponseBlocking_filterFetchResponse_DoNotUseDirectly();
|
||||
if (NS_WARN_IF(pref >
|
||||
static_cast<uint32_t>(OpaqueResponseFilterFetch::All))) {
|
||||
return OpaqueResponseFilterFetch::All;
|
||||
}
|
||||
|
||||
return static_cast<OpaqueResponseFilterFetch>(pref);
|
||||
}
|
||||
|
||||
HttpBaseChannel::HttpBaseChannel()
|
||||
: mReportCollector(new ConsoleReportCollector()),
|
||||
mHttpHandler(gHttpHandler),
|
||||
@ -3017,6 +3029,19 @@ nsresult HttpBaseChannel::ValidateMIMEType() {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool HttpBaseChannel::ShouldFilterOpaqueResponse(
|
||||
OpaqueResponseFilterFetch aFilterType) const {
|
||||
MOZ_DIAGNOSTIC_ASSERT(ShouldBlockOpaqueResponse());
|
||||
|
||||
if (!mLoadInfo || ConfiguredFilterFetchResponseBehaviour() != aFilterType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We should filter a response in the parent if it is opaque and is the result
|
||||
// of a fetch() function from the Fetch specification.
|
||||
return mLoadInfo->InternalContentPolicyType() == nsIContentPolicy::TYPE_FETCH;
|
||||
}
|
||||
|
||||
bool HttpBaseChannel::ShouldBlockOpaqueResponse() const {
|
||||
if (!mURI || !mResponseHead || !mLoadInfo) {
|
||||
// if there is no uri, no response head or no loadInfo, then there is
|
||||
@ -3036,6 +3061,7 @@ bool HttpBaseChannel::ShouldBlockOpaqueResponse() const {
|
||||
// Check if the response is a opaque response, which means requestMode should
|
||||
// be RequestMode::No_cors and responseType should be ResponseType::Opaque.
|
||||
nsContentPolicyType contentPolicy = mLoadInfo->InternalContentPolicyType();
|
||||
|
||||
// Skip the RequestMode would be RequestMode::Navigate
|
||||
if (contentPolicy == nsIContentPolicy::TYPE_DOCUMENT ||
|
||||
contentPolicy == nsIContentPolicy::TYPE_SUBDOCUMENT ||
|
||||
@ -3102,6 +3128,41 @@ bool HttpBaseChannel::ShouldBlockOpaqueResponse() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
OpaqueResponse HttpBaseChannel::BlockOrFilterOpaqueResponse(
|
||||
OpaqueResponseBlocker* aORB, const nsAString& aReason, const char* aFormat,
|
||||
...) {
|
||||
const bool shouldFilter =
|
||||
ShouldFilterOpaqueResponse(OpaqueResponseFilterFetch::BlockedByORB);
|
||||
|
||||
if (MOZ_UNLIKELY(MOZ_LOG_TEST(GetORBLog(), LogLevel::Debug))) {
|
||||
va_list ap;
|
||||
va_start(ap, aFormat);
|
||||
nsVprintfCString logString(aFormat, ap);
|
||||
va_end(ap);
|
||||
|
||||
LOGORB("%s: %s", shouldFilter ? "Filtered" : "Blocked", logString.get());
|
||||
}
|
||||
|
||||
if (shouldFilter) {
|
||||
// The existence of `mORB` depends on `BlockOrFilterOpaqueResponse` being
|
||||
// called before or after sniffing has completed.
|
||||
// Another requirement is that `OpaqueResponseFilter` must come after
|
||||
// `OpaqueResponseBlocker`, which is why in the case of having an
|
||||
// `OpaqueResponseBlocker` we let it handle creating an
|
||||
// `OpaqueResponseFilter`.
|
||||
if (aORB) {
|
||||
MOZ_DIAGNOSTIC_ASSERT(!mORB || aORB == mORB);
|
||||
aORB->FilterResponse();
|
||||
} else {
|
||||
mListener = new OpaqueResponseFilter(mListener);
|
||||
}
|
||||
return OpaqueResponse::Allow;
|
||||
}
|
||||
|
||||
LogORBError(aReason);
|
||||
return OpaqueResponse::Block;
|
||||
}
|
||||
|
||||
// The specification for ORB is currently being written:
|
||||
// https://whatpr.org/fetch/1442.html#orb-algorithm
|
||||
// The `opaque-response-safelist check` is implemented in:
|
||||
@ -3113,13 +3174,40 @@ OpaqueResponse
|
||||
HttpBaseChannel::PerformOpaqueResponseSafelistCheckBeforeSniff() {
|
||||
MOZ_ASSERT(XRE_IsParentProcess());
|
||||
|
||||
// https://whatpr.org/fetch/1442.html#http-fetch, step 6.4
|
||||
if (!ShouldBlockOpaqueResponse()) {
|
||||
return OpaqueResponse::Allow;
|
||||
}
|
||||
|
||||
// Regardless of if ORB is enabled or not, we check if we should filter the
|
||||
// response in the parent. This way data won't reach a content process that
|
||||
// will create a filtered `Response` object. This is enabled when
|
||||
// 'browser.opaqueResponseBlocking.filterFetchResponse' is
|
||||
// `OpaqueResponseFilterFetch::All`.
|
||||
// See https://fetch.spec.whatwg.org/#concept-filtered-response-opaque
|
||||
if (ShouldFilterOpaqueResponse(OpaqueResponseFilterFetch::All)) {
|
||||
mListener = new OpaqueResponseFilter(mListener);
|
||||
|
||||
// If we're filtering a response in the parent, there will be no data to
|
||||
// determine if it should be blocked or not so the only option we have is to
|
||||
// allow it.
|
||||
return OpaqueResponse::Allow;
|
||||
}
|
||||
|
||||
if (!mCachedOpaqueResponseBlockingPref) {
|
||||
return OpaqueResponse::Allow;
|
||||
}
|
||||
|
||||
// https://whatpr.org/fetch/1442.html#http-fetch, step 6.4
|
||||
if (!ShouldBlockOpaqueResponse()) {
|
||||
return OpaqueResponse::Allow;
|
||||
// If ORB is enabled, we check if we should filter the response in the parent.
|
||||
// This way data won't reach a content process that will create a filtered
|
||||
// `Response` object. We allow ORB to determine if the response should be
|
||||
// blocked or filtered, but regardless no data should reach the content
|
||||
// process. This is enabled when
|
||||
// 'browser.opaqueResponseBlocking.filterFetchResponse' is
|
||||
// `OpaqueResponseFilterFetch::AllowedByORB`.
|
||||
// See https://fetch.spec.whatwg.org/#concept-filtered-response-opaque
|
||||
if (ShouldFilterOpaqueResponse(OpaqueResponseFilterFetch::AllowedByORB)) {
|
||||
mListener = new OpaqueResponseFilter(mListener);
|
||||
}
|
||||
|
||||
Telemetry::ScalarAdd(
|
||||
@ -3147,24 +3235,22 @@ HttpBaseChannel::PerformOpaqueResponseSafelistCheckBeforeSniff() {
|
||||
// Step 3.1
|
||||
return OpaqueResponse::Allow;
|
||||
case OpaqueResponseBlockedReason::BLOCKED_BLOCKLISTED_NEVER_SNIFFED:
|
||||
// Step 3.2
|
||||
LOGORB("Blocked: BLOCKED_BLOCKLISTED_NEVER_SNIFFED");
|
||||
LogORBError(
|
||||
u"mimeType is an opaque-blocklisted-never-sniffed MIME type"_ns);
|
||||
return OpaqueResponse::Block;
|
||||
return BlockOrFilterOpaqueResponse(
|
||||
mORB, u"mimeType is an opaque-blocklisted-never-sniffed MIME type"_ns,
|
||||
"BLOCKED_BLOCKLISTED_NEVER_SNIFFED");
|
||||
case OpaqueResponseBlockedReason::BLOCKED_206_AND_BLOCKLISTED:
|
||||
// Step 3.3
|
||||
LOGORB("Blocked: BLOCKED_206_AND_BLOCKEDLISTED");
|
||||
LogORBError(
|
||||
u"response's status is 206 and mimeType is an opaque-blocklisted MIME type"_ns);
|
||||
return OpaqueResponse::Block;
|
||||
return BlockOrFilterOpaqueResponse(
|
||||
mORB,
|
||||
u"response's status is 206 and mimeType is an opaque-blocklisted MIME type"_ns,
|
||||
"BLOCKED_206_AND_BLOCKEDLISTED");
|
||||
case OpaqueResponseBlockedReason::
|
||||
BLOCKED_NOSNIFF_AND_EITHER_BLOCKLISTED_OR_TEXTPLAIN:
|
||||
// Step 3.4
|
||||
LOGORB("Blocked: BLOCKED_NOSNIFF_AND_EITHER_BLOCKLISTED_OR_TEXTPLAIN");
|
||||
LogORBError(
|
||||
u"nosniff is true and mimeType is an opaque-blocklisted MIME type or its essence is 'text/plain'"_ns);
|
||||
return OpaqueResponse::Block;
|
||||
return BlockOrFilterOpaqueResponse(
|
||||
mORB,
|
||||
u"nosniff is true and mimeType is an opaque-blocklisted MIME type or its essence is 'text/plain'"_ns,
|
||||
"BLOCKED_NOSNIFF_AND_EITHER_BLOCKLISTED_OR_TEXTPLAIN");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -3185,9 +3271,9 @@ HttpBaseChannel::PerformOpaqueResponseSafelistCheckBeforeSniff() {
|
||||
// Step 5
|
||||
if (mResponseHead->Status() == 206 &&
|
||||
!IsFirstPartialResponse(*mResponseHead)) {
|
||||
LOGORB("Blocked: Is not a valid partial response given 0");
|
||||
LogORBError(u"response status is 206 and not first partial response"_ns);
|
||||
return OpaqueResponse::Block;
|
||||
return BlockOrFilterOpaqueResponse(
|
||||
mORB, u"response status is 206 and not first partial response"_ns,
|
||||
"Is not a valid partial response given 0");
|
||||
}
|
||||
|
||||
// Setup for steps 6, 7, 8 and 10.
|
||||
@ -3242,25 +3328,22 @@ OpaqueResponse HttpBaseChannel::PerformOpaqueResponseSafelistCheckAfterSniff(
|
||||
bool isMediaRequest;
|
||||
mLoadInfo->GetIsMediaRequest(&isMediaRequest);
|
||||
if (isMediaRequest) {
|
||||
LOGORB("Blocked: media request");
|
||||
LogORBError(u"after sniff: media request"_ns);
|
||||
return OpaqueResponse::Block;
|
||||
return BlockOrFilterOpaqueResponse(mORB, u"after sniff: media request"_ns,
|
||||
"media request");
|
||||
}
|
||||
|
||||
// Step 11
|
||||
if (aNoSniff) {
|
||||
LOGORB("Blocked: nosniff");
|
||||
LogORBError(u"after sniff: nosniff is true"_ns);
|
||||
return OpaqueResponse::Block;
|
||||
return BlockOrFilterOpaqueResponse(mORB, u"after sniff: nosniff is true"_ns,
|
||||
"nosniff");
|
||||
}
|
||||
|
||||
// Step 12
|
||||
if (mResponseHead &&
|
||||
(mResponseHead->Status() < 200 || mResponseHead->Status() > 299)) {
|
||||
LOGORB("Blocked: status code (%d) is not allowed ",
|
||||
mResponseHead->Status());
|
||||
LogORBError(u"after sniff: status code is not in allowed range"_ns);
|
||||
return OpaqueResponse::Block;
|
||||
return BlockOrFilterOpaqueResponse(
|
||||
mORB, u"after sniff: status code is not in allowed range"_ns,
|
||||
"status code (%d) is not allowed", mResponseHead->Status());
|
||||
}
|
||||
|
||||
// Step 13
|
||||
@ -3273,10 +3356,10 @@ OpaqueResponse HttpBaseChannel::PerformOpaqueResponseSafelistCheckAfterSniff(
|
||||
if (StringBeginsWith(aContentType, "image/"_ns) ||
|
||||
StringBeginsWith(aContentType, "video/"_ns) ||
|
||||
StringBeginsWith(aContentType, "audio/"_ns)) {
|
||||
LOGORB("Blocked: ContentType is image/video/audio");
|
||||
LogORBError(
|
||||
u"after sniff: content-type declares image/video/audio, but sniffing fails"_ns);
|
||||
return OpaqueResponse::Block;
|
||||
return BlockOrFilterOpaqueResponse(
|
||||
mORB,
|
||||
u"after sniff: content-type declares image/video/audio, but sniffing fails"_ns,
|
||||
"ContentType is image/video/audio");
|
||||
}
|
||||
|
||||
return OpaqueResponse::Sniff;
|
||||
|
@ -10,6 +10,7 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "OpaqueResponseUtils.h"
|
||||
#include "mozilla/AtomicBitfields.h"
|
||||
#include "mozilla/Atomics.h"
|
||||
#include "mozilla/dom/DOMTypes.h"
|
||||
@ -41,6 +42,7 @@
|
||||
#include "nsIURI.h"
|
||||
#include "nsIUploadChannel2.h"
|
||||
#include "nsStringEnumerator.h"
|
||||
#include "nsStringFwd.h"
|
||||
#include "nsTArray.h"
|
||||
#include "nsThreadUtils.h"
|
||||
|
||||
@ -80,7 +82,9 @@ enum CacheDisposition : uint8_t {
|
||||
kCacheUnknown = 5
|
||||
};
|
||||
|
||||
enum class OpaqueResponse { Block, Allow, SniffCompressed, Sniff };
|
||||
// These need to be kept in sync with
|
||||
// "browser.opaqueResponseBlocking.filterFetchResponse"
|
||||
enum class OpaqueResponseFilterFetch { Never, AllowedByORB, BlockedByORB, All };
|
||||
|
||||
/*
|
||||
* This class is a partial implementation of nsIHttpChannel. It contains code
|
||||
@ -557,7 +561,7 @@ class HttpBaseChannel : public nsHashPropertyBag,
|
||||
// https://fetch.spec.whatwg.org/#concept-request-tainted-origin
|
||||
bool HasRedirectTaintedOrigin() { return LoadTaintedOriginFlag(); }
|
||||
|
||||
bool ChannelBlockedByOpaqueResponse() {
|
||||
bool ChannelBlockedByOpaqueResponse() const {
|
||||
return mChannelBlockedByOpaqueResponse;
|
||||
}
|
||||
bool CachedOpaqueResponseBlockingPref() const {
|
||||
@ -649,7 +653,11 @@ class HttpBaseChannel : public nsHashPropertyBag,
|
||||
|
||||
nsresult ValidateMIMEType();
|
||||
|
||||
bool ShouldFilterOpaqueResponse(OpaqueResponseFilterFetch aFilterType) const;
|
||||
bool ShouldBlockOpaqueResponse() const;
|
||||
OpaqueResponse BlockOrFilterOpaqueResponse(OpaqueResponseBlocker* aORB,
|
||||
const nsAString& aReason,
|
||||
const char* aFormat, ...);
|
||||
|
||||
OpaqueResponse PerformOpaqueResponseSafelistCheckBeforeSniff();
|
||||
|
||||
|
@ -1211,7 +1211,7 @@ HttpChannelParent::OnStartRequest(nsIRequest* aRequest) {
|
||||
responseHead = &cleanedUpResponseHead;
|
||||
}
|
||||
|
||||
if (chan && chan->ChannelBlockedByOpaqueResponse() &&
|
||||
if (chan->ChannelBlockedByOpaqueResponse() &&
|
||||
chan->CachedOpaqueResponseBlockingPref()) {
|
||||
responseHead->ClearHeaders();
|
||||
}
|
||||
|
@ -14,10 +14,13 @@
|
||||
#include "nsHttpResponseHead.h"
|
||||
#include "nsISupports.h"
|
||||
#include "nsMimeTypes.h"
|
||||
#include "nsStreamUtils.h"
|
||||
#include "nsThreadUtils.h"
|
||||
#include "nsStringStream.h"
|
||||
#include "HttpBaseChannel.h"
|
||||
|
||||
static mozilla::LazyLogModule gORBLog("ORB");
|
||||
|
||||
#define LOGORB(msg, ...) \
|
||||
MOZ_LOG(gORBLog, LogLevel::Debug, \
|
||||
("%s: %p " msg, __func__, this, ##__VA_ARGS__))
|
||||
@ -192,6 +195,52 @@ bool IsFirstPartialResponse(nsHttpResponseHead& aResponseHead) {
|
||||
return responseFirstBytePos == 0;
|
||||
}
|
||||
|
||||
LogModule* GetORBLog() { return gORBLog; }
|
||||
|
||||
OpaqueResponseFilter::OpaqueResponseFilter(nsIStreamListener* aNext)
|
||||
: mNext(aNext) {
|
||||
LOGORB();
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
OpaqueResponseFilter::OnStartRequest(nsIRequest* aRequest) {
|
||||
LOGORB();
|
||||
nsCOMPtr<HttpBaseChannel> httpBaseChannel = do_QueryInterface(aRequest);
|
||||
MOZ_ASSERT(httpBaseChannel);
|
||||
|
||||
nsHttpResponseHead* responseHead = httpBaseChannel->GetResponseHead();
|
||||
|
||||
if (responseHead) {
|
||||
// Filtered opaque responses doesn't need headers, so we just drop them.
|
||||
responseHead->ClearHeaders();
|
||||
}
|
||||
|
||||
mNext->OnStartRequest(aRequest);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
OpaqueResponseFilter::OnDataAvailable(nsIRequest* aRequest,
|
||||
nsIInputStream* aInputStream,
|
||||
uint64_t aOffset, uint32_t aCount) {
|
||||
LOGORB();
|
||||
uint32_t result;
|
||||
// No data for filtered opaque responses should reach the content process, so
|
||||
// we just discard them.
|
||||
return aInputStream->ReadSegments(NS_DiscardSegment, nullptr, aCount,
|
||||
&result);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
OpaqueResponseFilter::OnStopRequest(nsIRequest* aRequest,
|
||||
nsresult aStatusCode) {
|
||||
LOGORB();
|
||||
mNext->OnStopRequest(aRequest, aStatusCode);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMPL_ISUPPORTS(OpaqueResponseFilter, nsIStreamListener, nsIRequestObserver)
|
||||
|
||||
OpaqueResponseBlocker::OpaqueResponseBlocker(nsIStreamListener* aNext,
|
||||
HttpBaseChannel* aChannel,
|
||||
const nsCString& aContentType,
|
||||
@ -345,6 +394,17 @@ nsresult OpaqueResponseBlocker::EnsureOpaqueResponseIsAllowedAfterSniff(
|
||||
return ValidateJavaScript(httpBaseChannel, uri, loadInfo);
|
||||
}
|
||||
|
||||
OpaqueResponse
|
||||
OpaqueResponseBlocker::EnsureOpaqueResponseIsAllowedAfterJavaScriptValidation(
|
||||
HttpBaseChannel* aChannel, bool aAllow) {
|
||||
if (aAllow) {
|
||||
return OpaqueResponse::Allow;
|
||||
}
|
||||
|
||||
return aChannel->BlockOrFilterOpaqueResponse(
|
||||
this, u"Javascript validation failed"_ns, "Javascript validation failed");
|
||||
}
|
||||
|
||||
static void RecordTelemetry(const TimeStamp& aStartOfValidation,
|
||||
const TimeStamp& aStartOfJavaScriptValidation,
|
||||
OpaqueResponseBlocker::ValidatorResult aResult) {
|
||||
@ -428,12 +488,27 @@ nsresult OpaqueResponseBlocker::ValidateJavaScript(HttpBaseChannel* aChannel,
|
||||
uri->GetSpecOrDefault().get(),
|
||||
aSharedData.isSome() ? "true" : "false"));
|
||||
bool allowed = aResult == ValidatorResult::JavaScript;
|
||||
if (allowed) {
|
||||
self->AllowResponse();
|
||||
} else {
|
||||
self->BlockResponse(channel, NS_ERROR_FAILURE);
|
||||
channel->LogORBError(u"Javascript validation failed"_ns);
|
||||
switch (self->EnsureOpaqueResponseIsAllowedAfterJavaScriptValidation(
|
||||
channel, allowed)) {
|
||||
case OpaqueResponse::Allow:
|
||||
// It's possible that the JS validation failed for this request,
|
||||
// however we decided that we need to filter the response instead
|
||||
// of blocking. So we set allowed to true manually when that's the
|
||||
// case.
|
||||
allowed = true;
|
||||
self->AllowResponse();
|
||||
break;
|
||||
case OpaqueResponse::Block:
|
||||
self->BlockResponse(channel, NS_ERROR_FAILURE);
|
||||
break;
|
||||
default:
|
||||
MOZ_ASSERT_UNREACHABLE(
|
||||
"We should only ever have Allow or Block here.");
|
||||
allowed = false;
|
||||
self->BlockResponse(channel, NS_ERROR_FAILURE);
|
||||
break;
|
||||
}
|
||||
|
||||
self->ResolveAndProcessData(channel, allowed, aSharedData);
|
||||
if (aSharedData.isSome()) {
|
||||
self->mJSValidator->DeallocShmem(aSharedData.ref());
|
||||
@ -470,6 +545,18 @@ void OpaqueResponseBlocker::BlockResponse(HttpBaseChannel* aChannel,
|
||||
"OpaqueResponseBlocker::BlockResponse"_ns);
|
||||
}
|
||||
|
||||
void OpaqueResponseBlocker::FilterResponse() {
|
||||
MOZ_ASSERT(mState == State::Sniffing);
|
||||
|
||||
if (mShouldFilter) {
|
||||
return;
|
||||
}
|
||||
|
||||
mShouldFilter = true;
|
||||
|
||||
mNext = new OpaqueResponseFilter(mNext);
|
||||
}
|
||||
|
||||
void OpaqueResponseBlocker::ResolveAndProcessData(
|
||||
HttpBaseChannel* aChannel, bool aAllowed, Maybe<ipc::Shmem>& aSharedData) {
|
||||
nsresult rv = OnStartRequest(aChannel);
|
||||
|
@ -24,7 +24,6 @@
|
||||
#include "nsTArray.h"
|
||||
|
||||
class nsIContentSniffer;
|
||||
static mozilla::LazyLogModule gORBLog("ORB");
|
||||
|
||||
namespace mozilla::dom {
|
||||
class JSValidatorParent;
|
||||
@ -47,6 +46,8 @@ enum class OpaqueResponseBlockedReason : uint32_t {
|
||||
BLOCKED_SHOULD_SNIFF
|
||||
};
|
||||
|
||||
enum class OpaqueResponse { Block, Allow, SniffCompressed, Sniff };
|
||||
|
||||
OpaqueResponseBlockedReason GetOpaqueResponseBlockedReason(
|
||||
const nsACString& aContentType, uint16_t aStatus, bool aNoSniff);
|
||||
|
||||
@ -60,6 +61,24 @@ ParseContentRangeHeaderString(const nsAutoCString& aRangeStr);
|
||||
|
||||
bool IsFirstPartialResponse(nsHttpResponseHead& aResponseHead);
|
||||
|
||||
LogModule* GetORBLog();
|
||||
|
||||
// Helper class to filter data for opaque responses destined for `Window.fetch`.
|
||||
// See https://fetch.spec.whatwg.org/#concept-filtered-response-opaque.
|
||||
class OpaqueResponseFilter final : public nsIStreamListener {
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIREQUESTOBSERVER
|
||||
NS_DECL_NSISTREAMLISTENER;
|
||||
|
||||
explicit OpaqueResponseFilter(nsIStreamListener* aNext);
|
||||
|
||||
private:
|
||||
virtual ~OpaqueResponseFilter() = default;
|
||||
|
||||
nsCOMPtr<nsIStreamListener> mNext;
|
||||
};
|
||||
|
||||
class OpaqueResponseBlocker final : public nsIStreamListener {
|
||||
enum class State { Sniffing, Allowed, Blocked };
|
||||
|
||||
@ -74,9 +93,13 @@ class OpaqueResponseBlocker final : public nsIStreamListener {
|
||||
bool IsSniffing() const;
|
||||
void AllowResponse();
|
||||
void BlockResponse(HttpBaseChannel* aChannel, nsresult aStatus);
|
||||
void FilterResponse();
|
||||
|
||||
nsresult EnsureOpaqueResponseIsAllowedAfterSniff(nsIRequest* aRequest);
|
||||
|
||||
OpaqueResponse EnsureOpaqueResponseIsAllowedAfterJavaScriptValidation(
|
||||
HttpBaseChannel* aChannel, bool aAllow);
|
||||
|
||||
// The four possible results for validation. `JavaScript` and `JSON` are
|
||||
// self-explanatory. `JavaScript` is the only successful result, in the sense
|
||||
// that it will allow the opaque response, whereas `JSON` will block. `Other`
|
||||
@ -100,6 +123,7 @@ class OpaqueResponseBlocker final : public nsIStreamListener {
|
||||
|
||||
const nsCString mContentType;
|
||||
const bool mNoSniff;
|
||||
bool mShouldFilter = false;
|
||||
|
||||
State mState = State::Sniffing;
|
||||
nsresult mStatus = NS_OK;
|
||||
|
3
testing/web-platform/mozilla/meta/fetch/orb/__dir__.ini
Normal file
3
testing/web-platform/mozilla/meta/fetch/orb/__dir__.ini
Normal file
@ -0,0 +1,3 @@
|
||||
prefs: [browser.opaqueResponseBlocking: true,
|
||||
browser.opaqueResponseBlocking.filterFetchResponse:2]
|
||||
|
@ -0,0 +1,19 @@
|
||||
// META: script=/fetch/orb/resources/utils.js
|
||||
// META: script=resources/utils.js
|
||||
|
||||
const url =
|
||||
"http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources/image.png";
|
||||
|
||||
// Due to web compatibility we filter opaque Response object from the
|
||||
// fetch() function in the Fetch specification. See Bug 1823877. This
|
||||
// might be removed in the future.
|
||||
promise_internal_response_is_filtered(
|
||||
fetchORB(
|
||||
url,
|
||||
{ headers: new Headers([["Range", "bytes 10-99"]]) },
|
||||
header("Content-Range", "bytes 10-99/1010"),
|
||||
"slice(10,100)",
|
||||
"status(206)"
|
||||
),
|
||||
"ORB should filter opaque range of image/png not starting at zero, that isn't subsequent"
|
||||
);
|
@ -0,0 +1,48 @@
|
||||
// META: script=/fetch/orb/resources/utils.js
|
||||
// META: script=resources/utils.js
|
||||
|
||||
const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources";
|
||||
|
||||
// Due to web compatibility we filter opaque Response object from the
|
||||
// fetch() function in the Fetch specification. See Bug 1823877. This
|
||||
// might be removed in the future.
|
||||
promise_internal_response_is_filtered(
|
||||
fetchORB(`${path}/font.ttf`, null, contentType("font/ttf")),
|
||||
"ORB should filter opaque font/ttf"
|
||||
);
|
||||
|
||||
// Due to web compatibility we filter opaque Response object from the
|
||||
// fetch() function in the Fetch specification. See Bug 1823877. This
|
||||
// might be removed in the future.
|
||||
promise_internal_response_is_filtered(
|
||||
fetchORB(`${path}/text.txt`, null, contentType("text/plain")),
|
||||
"ORB should filter opaque text/plain"
|
||||
);
|
||||
|
||||
// Due to web compatibility we filter opaque Response object from the
|
||||
// fetch() function in the Fetch specification. See Bug 1823877. This
|
||||
// might be removed in the future.
|
||||
promise_internal_response_is_filtered(
|
||||
fetchORB(`${path}/data.json`, null, contentType("application/json")),
|
||||
"ORB should filter opaque application/json (non-empty)"
|
||||
);
|
||||
|
||||
// Due to web compatibility we filter opaque Response object from the
|
||||
// fetch() function in the Fetch specification. See Bug 1823877. This
|
||||
// might be removed in the future.
|
||||
promise_internal_response_is_filtered(
|
||||
fetchORB(`${path}/empty.json`, null, contentType("application/json")),
|
||||
"ORB should filter opaque application/json (empty)"
|
||||
);
|
||||
|
||||
// Due to web compatibility we filter opaque Response object from the
|
||||
// fetch() function in the Fetch specification. See Bug 1823877. This
|
||||
// might be removed in the future.
|
||||
promise_internal_response_is_filtered(
|
||||
fetchORB(
|
||||
`${path}/data_non_ascii.json`,
|
||||
null,
|
||||
contentType("application/json")
|
||||
),
|
||||
"ORB should filter opaque application/json which contains non ascii characters"
|
||||
);
|
@ -0,0 +1,30 @@
|
||||
// META: script=/fetch/orb/resources/utils.js
|
||||
// META: script=resources/utils.js
|
||||
|
||||
const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources";
|
||||
|
||||
// Due to web compatibility we filter opaque Response object from the
|
||||
// fetch() function in the Fetch specification. See Bug 1823877. This
|
||||
// might be removed in the future.
|
||||
promise_internal_response_is_filtered(
|
||||
fetchORB(
|
||||
`${path}/data.json`,
|
||||
null,
|
||||
contentType("application/json"),
|
||||
contentTypeOptions("nosniff")
|
||||
),
|
||||
"ORB should filter opaque-response-blocklisted MIME type with nosniff"
|
||||
);
|
||||
|
||||
// Due to web compatibility we filter opaque Response object from the
|
||||
// fetch() function in the Fetch specification. See Bug 1823877. This
|
||||
// might be removed in the future.
|
||||
promise_internal_response_is_filtered(
|
||||
fetchORB(
|
||||
`${path}/data.json`,
|
||||
null,
|
||||
contentType(""),
|
||||
contentTypeOptions("nosniff")
|
||||
),
|
||||
"ORB should filter opaque response with empty Content-Type and nosniff"
|
||||
);
|
@ -0,0 +1,21 @@
|
||||
function promise_internal_response_is_filtered(fetchPromise, message) {
|
||||
return promise_test(async () => {
|
||||
const response = await fetchPromise;
|
||||
|
||||
// A parent filtered opaque response is defined here as a response that isn't just an
|
||||
// opaque response, but also where the internal response has been made unavailable.
|
||||
// `Response.cloneUnfiltered` is used to inspect the state of the internal response,
|
||||
// which is exactly what we want to be missing in this case.
|
||||
const unfiltered = SpecialPowers.wrap(response).cloneUnfiltered();
|
||||
assert_equals(
|
||||
await SpecialPowers.unwrap(unfiltered).text(),
|
||||
"",
|
||||
"The internal response should be empty"
|
||||
);
|
||||
assert_equals(
|
||||
Array.from(await SpecialPowers.unwrap(unfiltered).headers).length,
|
||||
0,
|
||||
"The internal response should have no headers"
|
||||
);
|
||||
}, message);
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
// META: script=/fetch/orb/resources/utils.js
|
||||
// META: script=resources/utils.js
|
||||
|
||||
const path = "http://{{domains[www1]}}:{{ports[http][0]}}/fetch/orb/resources";
|
||||
|
||||
// Due to web compatibility we filter opaque Response object from the
|
||||
// fetch() function in the Fetch specification. See Bug 1823877. This
|
||||
// might be removed in the future.
|
||||
promise_internal_response_is_filtered(
|
||||
fetchORB(
|
||||
`${path}/data.json`,
|
||||
null,
|
||||
contentType("application/json"),
|
||||
"status(206)"
|
||||
),
|
||||
"ORB should filter opaque-response-blocklisted MIME type with status 206"
|
||||
);
|
||||
|
||||
// Due to web compatibility we filter opaque Response object from the
|
||||
// fetch() function in the Fetch specification. See Bug 1823877. This
|
||||
// might be removed in the future.
|
||||
promise_internal_response_is_filtered(
|
||||
fetchORB(
|
||||
`${path}/data.json`,
|
||||
null,
|
||||
contentType("application/json"),
|
||||
"status(302)"
|
||||
),
|
||||
"ORB should filter opaque range of image/png not starting at zero, that isn't subsequent"
|
||||
);
|
Loading…
x
Reference in New Issue
Block a user