ORB: Signal blocked resources by injecting a network error.
"ORB v0.1" handles response blocking CORB-style, by injecting an empty
response. This change adapts blocked response handling to the ORB
spec, by injecting a network error instead. This is controlled by a
feature flag (OpaqueResponseBlockingV02) to allow for staged rollout
and rollback.
Bug: 1178928
Change-Id: I54852059ceb04194158a22b98da87e4f52f8d514
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3785025
Reviewed-by: Matt Menke <mmenke@chromium.org>
Commit-Queue: Daniel Vogelheim <vogelheim@chromium.org>
Reviewed-by: Ćukasz Anforowicz <lukasza@chromium.org>
Reviewed-by: Philip Rogers <pdr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1048266}
diff --git a/content/browser/loader/cross_site_document_blocking_browsertest.cc b/content/browser/loader/cross_site_document_blocking_browsertest.cc
index b6a12b5..e500bcb 100644
--- a/content/browser/loader/cross_site_document_blocking_browsertest.cc
+++ b/content/browser/loader/cross_site_document_blocking_browsertest.cc
@@ -73,12 +73,18 @@
kShouldBeBlocked = 1 << 0,
kShouldBeSniffed = 1 << 1,
+ kBlockResourcesAsError = 1 << 2,
+
kShouldBeAllowedWithoutSniffing = 0,
kShouldBeBlockedWithoutSniffing = kShouldBeBlocked,
kShouldBeSniffedAndAllowed = kShouldBeSniffed,
kShouldBeSniffedAndBlocked = kShouldBeSniffed | kShouldBeBlocked,
};
+constexpr CorbExpectations BlockAsError(const CorbExpectations expectations) {
+ return CorbExpectations(expectations | kBlockResourcesAsError);
+}
+
std::ostream& operator<<(std::ostream& os, const CorbExpectations& value) {
if (value == 0) {
os << "(none)";
@@ -90,6 +96,8 @@
os << "kShouldBeBlocked ";
if (0 != (value & kShouldBeSniffed))
os << "kShouldBeSniffed ";
+ if (0 != (value & kBlockResourcesAsError))
+ os << "kBlockResourcesAsError ";
os << ")";
return os;
}
@@ -250,20 +258,31 @@
void Verify(CorbExpectations expectations,
const std::string& expected_resource_body) {
if (0 != (expectations & kShouldBeBlocked)) {
- ASSERT_EQ(net::OK, completion_status().error_code);
-
// Verify that the body is empty.
EXPECT_EQ("", response_body());
EXPECT_EQ(0, completion_status().decoded_body_length);
- // Verify that other response parts have been sanitized.
- EXPECT_EQ(0u, response_head()->content_length);
- const std::string& headers = response_head()->headers->raw_headers();
- EXPECT_THAT(headers, Not(HasSubstr("Content-Length")));
- EXPECT_THAT(headers, Not(HasSubstr("Content-Type")));
-
// Verify that the console message would have been printed.
EXPECT_TRUE(completion_status().should_report_corb_blocking);
+
+ // Verify the response code & headers, which depends on whether the
+ // response is blocked as an error, or as an empty response.
+ if (0 != (expectations & kBlockResourcesAsError)) {
+ ASSERT_EQ(net::ERR_BLOCKED_BY_ORB, completion_status().error_code);
+ ASSERT_FALSE(response_head());
+ } else {
+ ASSERT_EQ(net::OK, completion_status().error_code);
+
+ // Verify that response has no content.
+ EXPECT_EQ(0u, response_head()->content_length);
+
+ // Verify that response headers have been sanitized.
+ size_t iter = 0;
+ std::string name, value;
+ EXPECT_FALSE(response_head()->headers->EnumerateHeaderLines(
+ &iter, &name, &value));
+ EXPECT_EQ(iter, 0u);
+ }
} else {
ASSERT_EQ(net::OK, completion_status().error_code);
EXPECT_FALSE(completion_status().should_report_corb_blocking);
@@ -475,6 +494,7 @@
kWithCORBProtectionSniffing,
kWithoutCORBProtectionSniffing,
kWithORBv01,
+ kWithORBv02,
};
struct ImgTestParams {
const char* resource;
@@ -492,18 +512,30 @@
/* enabled_features= */ {network::features::
kCORBProtectionSniffing},
/* disabled_features= */ {
- network::features::kOpaqueResponseBlockingV01});
+ network::features::kOpaqueResponseBlockingV01,
+ network::features::kOpaqueResponseBlockingV02});
break;
case TestMode::kWithoutCORBProtectionSniffing:
scoped_feature_list_.InitWithFeatures(
/* enabled_features= */ {},
/* disabled_features= */ {
network::features::kCORBProtectionSniffing,
- network::features::kOpaqueResponseBlockingV01});
+ network::features::kOpaqueResponseBlockingV01,
+ network::features::kOpaqueResponseBlockingV02});
break;
case TestMode::kWithORBv01:
- scoped_feature_list_.InitAndEnableFeature(
- network::features::kOpaqueResponseBlockingV01);
+ scoped_feature_list_.InitWithFeatures(
+ /* enabled_features= */ {network::features::
+ kOpaqueResponseBlockingV01},
+ /* disabled_features= */ {
+ network::features::kOpaqueResponseBlockingV02});
+ break;
+ case TestMode::kWithORBv02:
+ scoped_feature_list_.InitWithFeatures(
+ /* enabled_features= */
+ {network::features::kOpaqueResponseBlockingV01,
+ network::features::kOpaqueResponseBlockingV02},
+ /* disabled_features= */ {});
break;
}
}
@@ -594,24 +626,32 @@
INSTANTIATE_TEST_SUITE_P( \
ORBv01_##tag, CrossSiteDocumentBlockingImgElementTest, \
::testing::Values( \
- ImgTestParams{resource, expectations, TestMode::kWithORBv01}));
+ ImgTestParams{resource, expectations, TestMode::kWithORBv01})); \
+ INSTANTIATE_TEST_SUITE_P( \
+ ORBv02_##tag, CrossSiteDocumentBlockingImgElementTest, \
+ ::testing::Values(ImgTestParams{resource, BlockAsError(expectations), \
+ TestMode::kWithORBv02}));
-#define IMG_TEST_WITH_DIFFERENT_ORB_EXPECTATIONS( \
- tag, resource, corb_expectations, orb_expectations) \
- INSTANTIATE_TEST_SUITE_P(ProtectionSniffingOn_##tag, \
- CrossSiteDocumentBlockingImgElementTest, \
- ::testing::Values(ImgTestParams{ \
- resource, corb_expectations, \
- TestMode::kWithoutCORBProtectionSniffing})); \
- INSTANTIATE_TEST_SUITE_P(ProtectionSniffingOff_##tag, \
- CrossSiteDocumentBlockingImgElementTest, \
- ::testing::Values(ImgTestParams{ \
- resource, corb_expectations, \
- TestMode::kWithCORBProtectionSniffing})); \
- INSTANTIATE_TEST_SUITE_P( \
- ORBv01_##tag, CrossSiteDocumentBlockingImgElementTest, \
- ::testing::Values( \
- ImgTestParams{resource, orb_expectations, TestMode::kWithORBv01}));
+#define IMG_TEST_WITH_DIFFERENT_ORB_EXPECTATIONS( \
+ tag, resource, corb_expectations, orb_expectations) \
+ INSTANTIATE_TEST_SUITE_P(ProtectionSniffingOn_##tag, \
+ CrossSiteDocumentBlockingImgElementTest, \
+ ::testing::Values(ImgTestParams{ \
+ resource, corb_expectations, \
+ TestMode::kWithoutCORBProtectionSniffing})); \
+ INSTANTIATE_TEST_SUITE_P(ProtectionSniffingOff_##tag, \
+ CrossSiteDocumentBlockingImgElementTest, \
+ ::testing::Values(ImgTestParams{ \
+ resource, corb_expectations, \
+ TestMode::kWithCORBProtectionSniffing})); \
+ INSTANTIATE_TEST_SUITE_P( \
+ ORBv01_##tag, CrossSiteDocumentBlockingImgElementTest, \
+ ::testing::Values( \
+ ImgTestParams{resource, orb_expectations, TestMode::kWithORBv01})); \
+ INSTANTIATE_TEST_SUITE_P( \
+ ORBv02_##tag, CrossSiteDocumentBlockingImgElementTest, \
+ ::testing::Values(ImgTestParams{ \
+ resource, BlockAsError(orb_expectations), TestMode::kWithORBv02}));
// The following are files under content/test/data/site_isolation. All
// should be disallowed for cross site XHR under the document blocking policy.
@@ -730,8 +770,18 @@
network::features::kCORBProtectionSniffing);
break;
case TestMode::kWithORBv01:
- scoped_feature_list_.InitAndEnableFeature(
- network::features::kOpaqueResponseBlockingV01);
+ scoped_feature_list_.InitWithFeatures(
+ /* enabled_features= */ {network::features::
+ kOpaqueResponseBlockingV01},
+ /* disabled_features= */ {
+ network::features::kOpaqueResponseBlockingV02});
+ break;
+ case TestMode::kWithORBv02:
+ scoped_feature_list_.InitWithFeatures(
+ /* enabled_features= */
+ {network::features::kOpaqueResponseBlockingV01,
+ network::features::kOpaqueResponseBlockingV02},
+ /* disabled_features= */ {});
break;
}
}
diff --git a/net/base/net_error_list.h b/net/base/net_error_list.h
index 772d699..20997e9 100644
--- a/net/base/net_error_list.h
+++ b/net/base/net_error_list.h
@@ -112,7 +112,7 @@
// The request failed because the response was delivered along with requirements
// which are not met ('X-Frame-Options' and 'Content-Security-Policy' ancestor
-// checks and 'Cross-Origin-Resource-Policy', for instance).
+// checks and 'Cross-Origin-Resource-Policy' for instance).
NET_ERROR(BLOCKED_BY_RESPONSE, -27)
// Error -28 was removed (BLOCKED_BY_XSS_AUDITOR).
@@ -127,6 +127,9 @@
// The request was blocked because of no H/2 or QUIC session.
NET_ERROR(H2_OR_QUIC_REQUIRED, -31)
+// The request was blocked by CORB or ORB.
+NET_ERROR(BLOCKED_BY_ORB, -32)
+
// A connection was closed (corresponding to a TCP FIN).
NET_ERROR(CONNECTION_CLOSED, -100)
diff --git a/services/network/public/cpp/corb/corb_api.cc b/services/network/public/cpp/corb/corb_api.cc
index 289e990..bde4d63 100644
--- a/services/network/public/cpp/corb/corb_api.cc
+++ b/services/network/public/cpp/corb/corb_api.cc
@@ -120,10 +120,11 @@
}
bool ShouldReportBlockedResponse() const override {
- if (is_orb_enabled_)
- return orb_analyzer_->ShouldReportBlockedResponse();
- else
- return corb_analyzer_->ShouldReportBlockedResponse();
+ return GetEnabledAnalyzer().ShouldReportBlockedResponse();
+ }
+
+ BlockedResponseHandling ShouldHandleBlockedResponseAs() const override {
+ return GetEnabledAnalyzer().ShouldHandleBlockedResponseAs();
}
private:
@@ -137,6 +138,13 @@
return is_orb_enabled_ ? orb_decision_ : corb_decision_;
}
+ const ResponseAnalyzer& GetEnabledAnalyzer() const {
+ if (is_orb_enabled_)
+ return *orb_analyzer_;
+ else
+ return *corb_analyzer_;
+ }
+
const std::unique_ptr<CrossOriginReadBlocking::CorbResponseAnalyzer>
corb_analyzer_;
const std::unique_ptr<OpaqueResponseBlockingAnalyzer> orb_analyzer_;
diff --git a/services/network/public/cpp/corb/corb_api.h b/services/network/public/cpp/corb/corb_api.h
index ada1a99..4f440cc 100644
--- a/services/network/public/cpp/corb/corb_api.h
+++ b/services/network/public/cpp/corb/corb_api.h
@@ -49,6 +49,13 @@
kSniffMore,
};
+ // Decision for how to signal a blocking decision to the network stack and
+ // the application.
+ enum class BlockedResponseHandling {
+ kEmptyResponse,
+ kNetworkError,
+ };
+
// The Init method should be called exactly once after getting the
// ResponseAnalyzer from the Create method. The Init method attempts to
// calculate the `Decision` based on the HTTP response headers. If
@@ -81,6 +88,9 @@
// warning message written to the DevTools console.
virtual bool ShouldReportBlockedResponse() const = 0;
+ // How should a blocked response be treated?
+ virtual BlockedResponseHandling ShouldHandleBlockedResponseAs() const = 0;
+
virtual ~ResponseAnalyzer();
};
diff --git a/services/network/public/cpp/corb/corb_impl.cc b/services/network/public/cpp/corb/corb_impl.cc
index 4b5d83c3..f387baf1 100644
--- a/services/network/public/cpp/corb/corb_impl.cc
+++ b/services/network/public/cpp/corb/corb_impl.cc
@@ -1029,6 +1029,13 @@
return true;
}
+ResponseAnalyzer::BlockedResponseHandling
+CrossOriginReadBlocking::CorbResponseAnalyzer::ShouldHandleBlockedResponseAs()
+ const {
+ // CORB wants blocked responses to be empty responses.
+ return ResponseAnalyzer::BlockedResponseHandling::kEmptyResponse;
+}
+
Decision CrossOriginReadBlocking::CorbResponseAnalyzer::GetCorbDecision() {
if (ShouldBlock())
return Decision::kBlock;
diff --git a/services/network/public/cpp/corb/corb_impl.h b/services/network/public/cpp/corb/corb_impl.h
index cb9d8dd..d925980 100644
--- a/services/network/public/cpp/corb/corb_impl.h
+++ b/services/network/public/cpp/corb/corb_impl.h
@@ -118,6 +118,7 @@
Decision Sniff(base::StringPiece data) override;
Decision HandleEndOfSniffableResponseBody() override;
bool ShouldReportBlockedResponse() const override;
+ BlockedResponseHandling ShouldHandleBlockedResponseAs() const override;
class ConfirmationSniffer;
class SimpleConfirmationSniffer;
diff --git a/services/network/public/cpp/corb/orb_impl.cc b/services/network/public/cpp/corb/orb_impl.cc
index 59a754d..17b32b23 100644
--- a/services/network/public/cpp/corb/orb_impl.cc
+++ b/services/network/public/cpp/corb/orb_impl.cc
@@ -14,6 +14,7 @@
#include "net/http/http_util.h"
#include "net/url_request/url_request.h"
#include "services/network/public/cpp/corb/corb_impl.h"
+#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
@@ -437,6 +438,16 @@
return !is_empty_response_ && is_http_status_okay_;
}
+ResponseAnalyzer::BlockedResponseHandling
+OpaqueResponseBlockingAnalyzer::ShouldHandleBlockedResponseAs() const {
+ // "ORB v0.1" uses CORB-style error handling with injecting an empty response.
+ // Later versions use ORB-specified error handling, by injecting a network
+ // error.
+ return base::FeatureList::IsEnabled(features::kOpaqueResponseBlockingV02)
+ ? BlockedResponseHandling::kNetworkError
+ : BlockedResponseHandling::kEmptyResponse;
+}
+
void OpaqueResponseBlockingAnalyzer::ReportOrbBlockedAndCorbDidnt() const {
// We encountered a scenario where ORB may block more than CORB and therefore
// let's log some extra data that may help us understand the kind of
diff --git a/services/network/public/cpp/corb/orb_impl.h b/services/network/public/cpp/corb/orb_impl.h
index 5cbe7acd..a406851 100644
--- a/services/network/public/cpp/corb/orb_impl.h
+++ b/services/network/public/cpp/corb/orb_impl.h
@@ -40,6 +40,7 @@
Decision Sniff(base::StringPiece data) override;
Decision HandleEndOfSniffableResponseBody() override;
bool ShouldReportBlockedResponse() const override;
+ BlockedResponseHandling ShouldHandleBlockedResponseAs() const override;
// TODO(https://crbug.com/1178928): Remove this once we gather enough
// DumpWithoutCrashing data.
diff --git a/services/network/public/cpp/features.cc b/services/network/public/cpp/features.cc
index c5a9c0c..67726d2 100644
--- a/services/network/public/cpp/features.cc
+++ b/services/network/public/cpp/features.cc
@@ -124,6 +124,13 @@
const base::Feature kOpaqueResponseBlockingV01{
"OpaqueResponseBlockingV01", base::FEATURE_DISABLED_BY_DEFAULT};
+// Enables ORB blocked responses being treated as errors (according to the spec)
+// rather than the current, CORB-style handling of injecting an empty response.
+// This is ORB v0.2.
+// This should only be enabled when ORB v0.1 is, too.
+const base::Feature kOpaqueResponseBlockingV02{
+ "OpaqueResponseBlockingV02", base::FEATURE_DISABLED_BY_DEFAULT};
+
// Enables preprocessing requests with the Trust Tokens API Fetch flags set,
// and handling their responses, according to the protocol.
// (See https://github.com/WICG/trust-token-api.)
diff --git a/services/network/public/cpp/features.h b/services/network/public/cpp/features.h
index 864f6f2..9d172c6 100644
--- a/services/network/public/cpp/features.h
+++ b/services/network/public/cpp/features.h
@@ -43,6 +43,8 @@
extern const base::Feature kMdnsResponderGeneratedNameListing;
COMPONENT_EXPORT(NETWORK_CPP)
extern const base::Feature kOpaqueResponseBlockingV01;
+COMPONENT_EXPORT(NETWORK_CPP)
+extern const base::Feature kOpaqueResponseBlockingV02;
COMPONENT_EXPORT(NETWORK_CPP)
extern const base::Feature kTrustTokens;
diff --git a/services/network/url_loader.cc b/services/network/url_loader.cc
index 58fb0a3..ea3404a 100644
--- a/services/network/url_loader.cc
+++ b/services/network/url_loader.cc
@@ -1679,31 +1679,14 @@
// Read Blocking / CORB).
if (factory_params_.is_corb_enabled) {
corb_analyzer_ = corb::ResponseAnalyzer::Create(per_factory_corb_state_);
+ is_more_corb_sniffing_needed_ = true;
auto decision =
corb_analyzer_->Init(url_request_->url(), url_request_->initiator(),
request_mode_, *response_);
- switch (decision) {
- case network::corb::ResponseAnalyzer::Decision::kBlock: {
- bool should_report_corb_blocking =
- corb_analyzer_->ShouldReportBlockedResponse();
- corb_analyzer_.reset();
- is_more_corb_sniffing_needed_ = false;
- if (BlockResponseForCorb(should_report_corb_blocking) ==
- kWillCancelRequest)
- return;
- break;
- }
-
- case network::corb::ResponseAnalyzer::Decision::kAllow:
- corb_analyzer_.reset();
- is_more_corb_sniffing_needed_ = false;
- break;
-
- case network::corb::ResponseAnalyzer::Decision::kSniffMore:
- is_more_corb_sniffing_needed_ = true;
- break;
- }
+ if (MaybeBlockResponseForCorb(decision))
+ return;
}
+
if ((options_ & mojom::kURLLoadOptionSniffMimeType)) {
if (ShouldSniffContent(url_request_->url(), *response_)) {
// We're going to look at the data before deciding what the content type
@@ -1847,24 +1830,8 @@
corb_decision);
}
- switch (corb_decision) {
- case network::corb::ResponseAnalyzer::Decision::kBlock: {
- bool should_report_corb_blocking =
- corb_analyzer_->ShouldReportBlockedResponse();
- corb_analyzer_.reset();
- is_more_corb_sniffing_needed_ = false;
- if (BlockResponseForCorb(should_report_corb_blocking) ==
- kWillCancelRequest)
- return;
- break;
- }
- case network::corb::ResponseAnalyzer::Decision::kAllow:
- corb_analyzer_.reset();
- is_more_corb_sniffing_needed_ = false;
- break;
- case network::corb::ResponseAnalyzer::Decision::kSniffMore:
- break;
- }
+ if (MaybeBlockResponseForCorb(corb_decision))
+ return;
}
}
@@ -2428,11 +2395,14 @@
memory_cache_writer_.reset();
}
-URLLoader::BlockResponseForCorbResult URLLoader::BlockResponseForCorb(
- bool should_report_corb_blocking) {
+URLLoader::BlockResponseForCorbResult URLLoader::BlockResponseForCorb() {
// CORB should only do work after the response headers have been received.
DCHECK(has_received_response_);
+ // Caller should have set up a CorbAnalyzer for BlockResponseForCorb to be
+ // able to do its job.
+ DCHECK(corb_analyzer_);
+
// The response headers and body shouldn't yet be sent to the URLLoaderClient.
DCHECK(response_);
DCHECK(consumer_handle_.is_valid());
@@ -2440,34 +2410,54 @@
// Send stripped headers to the real URLLoaderClient.
corb::SanitizeBlockedResponseHeaders(*response_);
- // Send empty body to the real URLLoaderClient.
- mojo::ScopedDataPipeProducerHandle producer_handle;
- mojo::ScopedDataPipeConsumerHandle consumer_handle;
- MojoResult result = mojo::CreateDataPipe(kBlockedBodyAllocationSize,
- producer_handle, consumer_handle);
- if (result != MOJO_RESULT_OK) {
- NotifyCompleted(net::ERR_INSUFFICIENT_RESOURCES);
- return kWillCancelRequest;
- }
- producer_handle.reset();
+ // Determine error code. This essentially handles the "ORB v0.1" and "ORB
+ // v0.2" difference.
+ int blocked_error_code =
+ (corb_analyzer_->ShouldHandleBlockedResponseAs() ==
+ corb::ResponseAnalyzer::BlockedResponseHandling::kEmptyResponse)
+ ? net::OK
+ : net::ERR_BLOCKED_BY_ORB;
- url_loader_client_.Get()->OnReceiveResponse(
- response_->Clone(), std::move(consumer_handle), absl::nullopt);
-
- // Tell the real URLLoaderClient that the response has been completed.
- if (corb_detachable_) {
- // TODO(lukasza): https://crbug.com/827633#c5: Consider passing net::ERR_OK
- // instead. net::ERR_ABORTED was chosen for consistency with the old CORB
- // implementation that used to go through DetachableResourceHandler.
- CompleteBlockedResponse(net::ERR_ABORTED, should_report_corb_blocking);
- } else {
- // CORB responses are reported as a success.
- CompleteBlockedResponse(net::OK, should_report_corb_blocking);
+ // todo(lukasza/vogelheim): https://crbug.com/827633#c5:
+ // This preserves compatibility with current implementations, which use
+ // net::ERR_ABORTED when the resource is detachable. We will not carry this
+ // forward past "ORB v0.1", so that this check will go away once
+ // OpaqueResponseBlockingV02 is perma-enabled.
+ if (corb_detachable_ && blocked_error_code == net::OK) {
+ CHECK(!base::FeatureList::IsEnabled(features::kOpaqueResponseBlockingV02));
+ blocked_error_code = net::ERR_ABORTED;
}
+ // Send empty body to the real URLLoaderClient. This preserves "ORB v0.1"
+ // behaviour and will also go away once OpaqueResponseBlockingV02 is
+ // perma-enabled.
+ if (blocked_error_code == net::OK || blocked_error_code == net::ERR_ABORTED) {
+ mojo::ScopedDataPipeProducerHandle producer_handle;
+ mojo::ScopedDataPipeConsumerHandle consumer_handle;
+ MojoResult result = mojo::CreateDataPipe(kBlockedBodyAllocationSize,
+ producer_handle, consumer_handle);
+ if (result != MOJO_RESULT_OK) {
+ NotifyCompleted(net::ERR_INSUFFICIENT_RESOURCES);
+ return kWillCancelRequest;
+ }
+ producer_handle.reset();
+
+ // Tell the real URLLoaderClient that the response has been completed.
+ url_loader_client_.Get()->OnReceiveResponse(
+ response_->Clone(), std::move(consumer_handle), absl::nullopt);
+ }
+
+ CompleteBlockedResponse(blocked_error_code,
+ corb_analyzer_->ShouldReportBlockedResponse());
+
// If the factory is asking to complete requests of this type, then we need to
// continue processing the response to make sure the network cache is
// populated. Otherwise we can cancel the request.
+ //
+ // TODO(lukasza/vogelheim): The `corb_detachable_` logic is meant to ensure a
+ // response is cached (in some cases). With HTTP cache partitioning, this is
+ // likely much less effective than it used to be. Maybe this mechanism should
+ // be retired.
if (corb_detachable_) {
// Discard any remaining callbacks or data by rerouting the pipes to
// EmptyURLLoaderClient.
@@ -2497,6 +2487,29 @@
return kWillCancelRequest;
}
+bool URLLoader::MaybeBlockResponseForCorb(
+ corb::ResponseAnalyzer::Decision corb_decision) {
+ DCHECK(corb_analyzer_);
+ DCHECK(is_more_corb_sniffing_needed_);
+ bool will_cancel = false;
+ switch (corb_decision) {
+ case network::corb::ResponseAnalyzer::Decision::kBlock: {
+ will_cancel = BlockResponseForCorb() == kWillCancelRequest;
+ corb_analyzer_.reset();
+ is_more_corb_sniffing_needed_ = false;
+ break;
+ }
+ case network::corb::ResponseAnalyzer::Decision::kAllow:
+ corb_analyzer_.reset();
+ is_more_corb_sniffing_needed_ = false;
+ break;
+ case network::corb::ResponseAnalyzer::Decision::kSniffMore:
+ break;
+ }
+ DCHECK_EQ(is_more_corb_sniffing_needed_, !!corb_analyzer_);
+ return will_cancel;
+}
+
void URLLoader::ReportFlaggedResponseCookies() {
if (cookie_observer_) {
std::vector<mojom::CookieOrLineWithAccessResultPtr> reported_cookies;
diff --git a/services/network/url_loader.h b/services/network/url_loader.h
index 0aa6aa95..798e7a7 100644
--- a/services/network/url_loader.h
+++ b/services/network/url_loader.h
@@ -419,8 +419,11 @@
// processing the request (e.g. by calling ReadMore as necessary).
kContinueRequest,
};
- BlockResponseForCorbResult BlockResponseForCorb(
- bool should_report_corb_blocking);
+ // Block the response because of CORB (or ORB).
+ BlockResponseForCorbResult BlockResponseForCorb();
+ // Decide whether to call block a response via BlockResponseForCorb.
+ // Returns true if the request should be cancelled.
+ bool MaybeBlockResponseForCorb(corb::ResponseAnalyzer::Decision);
void ReportFlaggedResponseCookies();
void StartReading();
diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites
index 7031c95..f221aeb 100644
--- a/third_party/blink/web_tests/VirtualTestSuites
+++ b/third_party/blink/web_tests/VirtualTestSuites
@@ -1273,5 +1273,23 @@
"external/wpt/close-watcher"
],
"args": ["--enable-blink-features=CloseWatcher"]
+ },
+ {
+ "prefix": "orb-v01",
+ "platforms": ["Linux"],
+ "args": ["--enable-features=OpaqueResponseBlockingV01"],
+ "bases": [
+ "external/wpt/fetch/corb",
+ "external/wpt/fetch/api",
+ "external/wpt/fetch/nosniff"
+ ]
+ },
+ {
+ "prefix": "orb-v02",
+ "platforms": ["Linux"],
+ "args": ["--enable-features=OpaqueResponseBlockingV01,OpaqueResponseBlockingV02"],
+ "bases": [
+ "external/wpt/fetch/corb"
+ ]
}
]
diff --git a/third_party/blink/web_tests/external/wpt/fetch/corb/resources/response_block_probe.js b/third_party/blink/web_tests/external/wpt/fetch/corb/resources/response_block_probe.js
new file mode 100644
index 0000000..d23ad48
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/fetch/corb/resources/response_block_probe.js
@@ -0,0 +1 @@
+window.script_callback();
diff --git a/third_party/blink/web_tests/external/wpt/fetch/corb/resources/response_block_probe.js.headers b/third_party/blink/web_tests/external/wpt/fetch/corb/resources/response_block_probe.js.headers
new file mode 100644
index 0000000..0d848b0
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/fetch/corb/resources/response_block_probe.js.headers
@@ -0,0 +1 @@
+Content-Type: text/csv
diff --git a/third_party/blink/web_tests/external/wpt/fetch/corb/response_block.tentative.sub.https-expected.txt b/third_party/blink/web_tests/external/wpt/fetch/corb/response_block.tentative.sub.https-expected.txt
new file mode 100644
index 0000000..5f3cd87
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/fetch/corb/response_block.tentative.sub.https-expected.txt
@@ -0,0 +1,5 @@
+This is a testharness.js-based test.
+FAIL ORB: Expect error response. assert_equals: expected "script errored" but got "script loaded"
+FAIL !CORB: CORB would have expected an empty response. assert_not_equals: got disallowed value "script loaded"
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/external/wpt/fetch/corb/response_block.tentative.sub.https.html b/third_party/blink/web_tests/external/wpt/fetch/corb/response_block.tentative.sub.https.html
new file mode 100644
index 0000000..860e0d3
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/fetch/corb/response_block.tentative.sub.https.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<body>
+<script>
+// Test handling of blocked responses in CORB/ORB.
+function probe() {
+ // We will cross-origin load a script resource that should get blocked by all
+ // versions of CORB/ORB. Three things may happen:
+ //
+ // 1, The script might execute. (CORB/ORB not supported. Or a bug.)
+ // 2, An empty response is injected: The script loads but nothing is executed.
+ // (Expected behaviour for CORB and "ORB v0.1".)
+ // 3, An error is injected and script loading aborts. (Expected for ORB.)
+
+ // A cross-origin response labelled as text/csv, which will call
+ // script_callback when executed.
+ const probe = "https://{{domains[www1]}}:{{ports[https][0]}}" +
+ "/fetch/corb/resources/response_block_probe.js";
+
+ // Load the probe as a script.
+ const script = document.createElement("script");
+ script.src = probe;
+ document.body.appendChild(script);
+
+ // Return a promise that will return a string description corresponding to the
+ // three conditions above. Not that a script_callback call is processed
+ // synchronously and hence will occur before the onload event is dispatched.
+ return new Promise((resolve, reject) => {
+ script.onload = _ => resolve("script loaded");
+ script.onerror = _ => resolve("script errored");
+ window.script_callback = _ => resolve("script executed");
+ });
+}
+
+promise_test(t => probe().then(
+ value => assert_equals(value, "script errored")),
+ "ORB: Expect error response.");
+
+// This test ensures we're _not_ seeing CORB behaviour.
+promise_test(t => probe().then(
+ value => assert_not_equals(value, "script loaded")),
+ "!CORB: CORB would have expected an empty response.");
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/fetch/corb/script-html-correctly-labeled.tentative.sub.html b/third_party/blink/web_tests/external/wpt/fetch/corb/script-html-correctly-labeled.tentative.sub.html
index 8f4d767..6d1947c 100644
--- a/third_party/blink/web_tests/external/wpt/fetch/corb/script-html-correctly-labeled.tentative.sub.html
+++ b/third_party/blink/web_tests/external/wpt/fetch/corb/script-html-correctly-labeled.tentative.sub.html
@@ -14,11 +14,13 @@
// Without CORB, the html document would cause a syntax error when parsed as
// JavaScript, but with CORB there should be no errors (because CORB will
- // replace the response body with an empty body).
- script.onload = t.step_func_done(function(){})
+ // replace the response body with an empty body). With ORB, the script loading
+ // itself will error out.
+ script.onload = t.step_func_done();
+ script.onerror = t.step_func_done();
addEventListener("error",function(e) {
t.step(function() {
- assert_unreached("Empty body of a CORS-blocked response shouldn't trigger syntax errors.");
+ assert_unreached("Empty body of a CORB-blocked response shouldn't trigger syntax errors.");
t.done();
})
});
diff --git a/third_party/blink/web_tests/external/wpt/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html b/third_party/blink/web_tests/external/wpt/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html
index 1d9af35..f0eb1f0a 100644
--- a/third_party/blink/web_tests/external/wpt/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html
+++ b/third_party/blink/web_tests/external/wpt/fetch/corb/script-resource-with-json-parser-breaker.tentative.sub.html
@@ -62,8 +62,10 @@
// Without CORB, the JSON parser breaker would cause a syntax error when
// parsed as JavaScript, but with CORB there should be no errors (because
- // CORB will replace the response body with an empty body).
+ // CORB will replace the response body with an empty body). With ORB,
+ // the script loading itself should error out.
script.onload = resolve;
+ script.onerror = resolve;
addEventListener("error", t.unreached_func(
"Empty body of a CORS-blocked response shouldn't trigger syntax errors."))
diff --git a/third_party/blink/web_tests/external/wpt/fetch/range/resources/partial-text.py b/third_party/blink/web_tests/external/wpt/fetch/range/resources/partial-text.py
index a005855..fa3d1171 100644
--- a/third_party/blink/web_tests/external/wpt/fetch/range/resources/partial-text.py
+++ b/third_party/blink/web_tests/external/wpt/fetch/range/resources/partial-text.py
@@ -8,12 +8,13 @@
def main(request, response):
total_length = int(request.GET.first(b'length', b'100'))
partial_code = int(request.GET.first(b'partial', b'206'))
+ content_type = request.GET.first(b'type', b'text/plain')
range_header = request.headers.get(b'Range', b'')
# Send a 200 if there is no range request
if not range_header:
to_send = ''.zfill(total_length)
- response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"Content-Type", content_type)
response.headers.set(b"Cache-Control", b"no-cache")
response.headers.set(b"Content-Length", total_length)
response.content = to_send
@@ -29,12 +30,17 @@
# Error the request if the range goes beyond the length
if length <= 0 or end > total_length:
response.set_error(416, u"Range Not Satisfiable")
+ # set_error sets the MIME type to application/json, which - for a
+ # no-cors media request - will be blocked by ORB. We'll just force
+ # the expected MIME type here, whichfixes the test, but doesn't make
+ # sense in general.
+ response.headers = [(b"Content-Type", content_type)]
response.write()
return
# Generate a partial response of the requested length
to_send = ''.zfill(length)
- response.headers.set(b"Content-Type", b"text/plain")
+ response.headers.set(b"Content-Type", content_type)
response.headers.set(b"Accept-Ranges", b"bytes")
response.headers.set(b"Cache-Control", b"no-cache")
response.status = partial_code
diff --git a/third_party/blink/web_tests/external/wpt/fetch/range/sw.https.window.js b/third_party/blink/web_tests/external/wpt/fetch/range/sw.https.window.js
index 42e4ac6..62ad894d 100644
--- a/third_party/blink/web_tests/external/wpt/fetch/range/sw.https.window.js
+++ b/third_party/blink/web_tests/external/wpt/fetch/range/sw.https.window.js
@@ -164,7 +164,7 @@
const rangeId = Math.random() + '';
const rangeBroadcast = awaitMessage(w.navigator.serviceWorker, rangeId);
- // Create a bogus audo element to trick the browser into sending
+ // Create a bogus audio element to trick the browser into sending
// cross-origin range requests that can be manipulated by the service worker.
const sound_url = new URL('partial-text.py', w.location);
sound_url.hostname = REMOTE_HOST;
@@ -173,6 +173,7 @@
sound_url.searchParams.set('size', size);
sound_url.searchParams.set('partial', partialResponseCode);
sound_url.searchParams.set('id', rangeId);
+ sound_url.searchParams.set('type', 'audio/mp4');
appendAudio(w.document, sound_url);
// wait for the range requests to happen
@@ -184,6 +185,7 @@
const url = new URL('partial-text.py', w.location);
url.searchParams.set('action', 'use-media-range-request');
url.searchParams.set('size', size);
+ url.searchParams.set('type', 'audio/mp4');
counts['size' + size] = 0;
for (let i = 0; i < count; i++) {
await preloadImage(url, { doc: w.document });
diff --git a/third_party/blink/web_tests/virtual/orb-v01/README.md b/third_party/blink/web_tests/virtual/orb-v01/README.md
new file mode 100644
index 0000000..e42ddd9
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/orb-v01/README.md
@@ -0,0 +1,5 @@
+# Test suite for OpaqueResponseBlockingV01 (ORB v0.1)
+
+Since this feature is Fetch-related, this suite tests all WPT fetch tests
+with `--enable-features=OpaqueResponseBlockingV01`. Tests which are expected
+to behave differently for ORB v0.1 have separate expectations.
diff --git a/third_party/blink/web_tests/virtual/orb-v01/external/wpt/fetch/README.txt b/third_party/blink/web_tests/virtual/orb-v01/external/wpt/fetch/README.txt
new file mode 100644
index 0000000..c0f4ca6
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/orb-v01/external/wpt/fetch/README.txt
@@ -0,0 +1,6 @@
+Test Expectations for virtual/orb-v01.
+
+corb/response_block.tentative.sub.https.html tests behaviour that is different
+between CORB, ORB v0.1 and ORB v0.2 (and full ORB), and hence has separate
+expecations in this virtual test suite.
+
diff --git a/third_party/blink/web_tests/virtual/orb-v01/external/wpt/fetch/corb/response_block.tentative.sub.https-expected.txt b/third_party/blink/web_tests/virtual/orb-v01/external/wpt/fetch/corb/response_block.tentative.sub.https-expected.txt
new file mode 100644
index 0000000..5f3cd87
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/orb-v01/external/wpt/fetch/corb/response_block.tentative.sub.https-expected.txt
@@ -0,0 +1,5 @@
+This is a testharness.js-based test.
+FAIL ORB: Expect error response. assert_equals: expected "script errored" but got "script loaded"
+FAIL !CORB: CORB would have expected an empty response. assert_not_equals: got disallowed value "script loaded"
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/virtual/orb-v02/README.md b/third_party/blink/web_tests/virtual/orb-v02/README.md
new file mode 100644
index 0000000..1ef506e
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/orb-v02/README.md
@@ -0,0 +1,6 @@
+# Test suite for OpaqueResponseBlockingV02 (ORB v0.2)
+
+Since this feature is Fetch-related, this suite tests all WPT fetch tests
+with `--enable-features=OpaqueResponseBlockingV01,OpaqueResponseBlockingV02`.
+Tests which are expected to behave differently for ORB v0.1 have separate
+expectations.
diff --git a/third_party/blink/web_tests/virtual/orb-v02/external/wpt/fetch/README.txt b/third_party/blink/web_tests/virtual/orb-v02/external/wpt/fetch/README.txt
new file mode 100644
index 0000000..24dacfb8
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/orb-v02/external/wpt/fetch/README.txt
@@ -0,0 +1,5 @@
+Test Expectations for virtual/orb-v02.
+
+corb/response_block.tentative.sub.https.html tests behaviour that is different
+between CORB, ORB v0.1 and ORB v0.2 (and full ORB), and hence has separate
+expecations in this virtual test suite.
diff --git a/third_party/blink/web_tests/virtual/orb-v02/external/wpt/fetch/corb/response_block.tentative.sub.https-expected.txt b/third_party/blink/web_tests/virtual/orb-v02/external/wpt/fetch/corb/response_block.tentative.sub.https-expected.txt
new file mode 100644
index 0000000..d9bfbc7
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/orb-v02/external/wpt/fetch/corb/response_block.tentative.sub.https-expected.txt
@@ -0,0 +1,5 @@
+This is a testharness.js-based test.
+PASS ORB: Expect error response.
+PASS !CORB: CORB would have expected an empty response.
+Harness: the test ran to completion.
+