Skip to content

Commit

Permalink
[M90] Javascript parsing UMA for Opaque Response Blocking.
Browse files Browse the repository at this point in the history
This CL implements a *subset* of the heuristics/steps of the Opaque
Response Blocking (aka ORB / CORB++) algorithm [1].  The implemented
subset is sufficient for calculating a UMA metric that will estimate an
upper bound on the percentage of no-cors requests that would require
parsing their full body as JavaScript before passing the response body
to a renderer process.

[1] https://github.com/annevk/orb

(cherry picked from commit 5be9719)

Change-Id: I4d4d75c34040f62147f28cdb9d86750bfbd8c252
Bug: 1178928
Fixes: 1182873
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2693532
Auto-Submit: Łukasz Anforowicz <lukasza@chromium.org>
Reviewed-by: Caitlin Fischer <caitlinfischer@google.com>
Reviewed-by: Matt Menke <mmenke@chromium.org>
Commit-Queue: Łukasz Anforowicz <lukasza@chromium.org>
Cr-Original-Commit-Position: refs/heads/master@{#858278}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2742074
Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Cr-Commit-Position: refs/branch-heads/4430@{#227}
Cr-Branched-From: e5ce7dc-refs/heads/master@{#857950}
  • Loading branch information
anforowicz authored and Chromium LUCI CQ committed Mar 8, 2021
1 parent 45195a1 commit 600fb32
Show file tree
Hide file tree
Showing 9 changed files with 656 additions and 25 deletions.
3 changes: 3 additions & 0 deletions services/network/public/cpp/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ component("cpp") {
"network_switches.h",
"not_implemented_url_loader_factory.cc",
"not_implemented_url_loader_factory.h",
"opaque_response_blocking.cc",
"opaque_response_blocking.h",
"origin_agent_cluster_parser.cc",
"origin_agent_cluster_parser.h",
"parsed_headers.cc",
Expand Down Expand Up @@ -369,6 +371,7 @@ source_set("tests") {
"network_isolation_key_mojom_traits_unittest.cc",
"network_mojom_traits_unittest.cc",
"network_quality_tracker_unittest.cc",
"opaque_response_blocking_unittest.cc",
"optional_trust_token_params_unittest.cc",
"origin_agent_cluster_parser_unittest.cc",
"proxy_config_mojom_traits_unittest.cc",
Expand Down
52 changes: 33 additions & 19 deletions services/network/public/cpp/cross_origin_read_blocking.cc
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,18 @@ const auto& GetNeverSniffedMimeTypes() {

} // namespace

// static
bool CrossOriginReadBlocking::IsJavascriptMimeType(
base::StringPiece mime_type) {
constexpr auto kCaseInsensitive = base::CompareCase::INSENSITIVE_ASCII;
for (const std::string& suffix : kJavaScriptSuffixes) {
if (base::EndsWith(mime_type, suffix, kCaseInsensitive))
return true;
}

return false;
}

// static
MimeType CrossOriginReadBlocking::GetCanonicalMimeType(
base::StringPiece mime_type) {
Expand Down Expand Up @@ -788,9 +800,13 @@ CrossOriginReadBlocking::ResponseAnalyzer::ShouldBlockBasedOnHeaders(
// Requests from foo.example.com will consult foo.example.com's service worker
// first (if one has been registered). The service worker can handle requests
// initiated by foo.example.com even if they are cross-origin (e.g. requests
// for bar.example.com). This is okay and should not be blocked by CORB,
// unless the initiator opted out of CORS / opted into receiving an opaque
// response. See also https://crbug.com/803672.
// for bar.example.com). This is okay, because there is no security boundary
// between foo.example.com and the service worker of foo.example.com + because
// the response data is "conjured" within the service worker of
// foo.example.com (rather than being fetched from bar.example.com).
// Therefore such responses should not be blocked by CORB, unless the
// initiator opted out of CORS / opted into receiving an opaque response. See
// also https://crbug.com/803672.
if (response.was_fetched_via_service_worker) {
switch (response.response_type) {
case network::mojom::FetchResponseType::kBasic:
Expand Down Expand Up @@ -901,17 +917,6 @@ CrossOriginReadBlocking::ResponseAnalyzer::ShouldBlockBasedOnHeaders(
return kBlock;
}

// static
bool CrossOriginReadBlocking::ResponseAnalyzer::HasNoSniff(
const network::mojom::URLResponseHead& response) {
if (!response.headers)
return false;
std::string nosniff_header;
response.headers->GetNormalizedHeader("x-content-type-options",
&nosniff_header);
return base::LowerCaseEqualsASCII(nosniff_header, "nosniff");
}

// static
bool CrossOriginReadBlocking::ResponseAnalyzer::SeemsSensitiveFromCORSHeuristic(
const network::mojom::URLResponseHead& response) {
Expand Down Expand Up @@ -984,15 +989,13 @@ CrossOriginReadBlocking::ResponseAnalyzer::GetMimeTypeBucket(

// Javascript is assumed public. See also
// https://mimesniff.spec.whatwg.org/#javascript-mime-type.
constexpr auto kCaseInsensitive = base::CompareCase::INSENSITIVE_ASCII;
for (const std::string& suffix : kJavaScriptSuffixes) {
if (base::EndsWith(mime_type, suffix, kCaseInsensitive)) {
return kPublic;
}
if (IsJavascriptMimeType(mime_type)) {
return kPublic;
}

// Images are assumed public. See also
// https://mimesniff.spec.whatwg.org/#image-mime-type.
constexpr auto kCaseInsensitive = base::CompareCase::INSENSITIVE_ASCII;
if (base::StartsWith(mime_type, "image", kCaseInsensitive)) {
return kPublic;
}
Expand Down Expand Up @@ -1186,6 +1189,17 @@ void CrossOriginReadBlocking::ResponseAnalyzer::LogBlockedResponse() {
canonical_mime_type_);
}

// static
bool CrossOriginReadBlocking::ResponseAnalyzer::HasNoSniff(
const network::mojom::URLResponseHead& response) {
if (!response.headers)
return false;
std::string nosniff_header;
response.headers->GetNormalizedHeader("x-content-type-options",
&nosniff_header);
return base::LowerCaseEqualsASCII(nosniff_header, "nosniff");
}

// static
CrossOriginReadBlocking::ResponseAnalyzer::CrossOriginProtectionDecision
CrossOriginReadBlocking::ResponseAnalyzer::BlockingDecisionToProtectionDecision(
Expand Down
16 changes: 10 additions & 6 deletions services/network/public/cpp/cross_origin_read_blocking.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ namespace network {

class COMPONENT_EXPORT(NETWORK_CPP) CrossOriginReadBlocking {
public:
// Not instantiable - only static methods.
CrossOriginReadBlocking() = delete;

// This enum describes how CORB should decide whether to block a given
// no-cors, cross-origin response.
//
Expand Down Expand Up @@ -151,6 +154,9 @@ class COMPONENT_EXPORT(NETWORK_CPP) CrossOriginReadBlocking {
void LogAllowedResponse();
void LogBlockedResponse();

// Returns true if the response has a nosniff header.
static bool HasNoSniff(const network::mojom::URLResponseHead& response);

private:
FRIEND_TEST_ALL_PREFIXES(CrossOriginReadBlockingTest,
SeemsSensitiveFromCORSHeuristic);
Expand Down Expand Up @@ -184,9 +190,6 @@ class COMPONENT_EXPORT(NETWORK_CPP) CrossOriginReadBlocking {
const base::Optional<url::Origin>& request_initiator_origin_lock,
MimeType canonical_mime_type);

// Returns true if the response has a nosniff header.
static bool HasNoSniff(const network::mojom::URLResponseHead& response);

// Checks if the response seems sensitive for CORB protection logging.
// Returns true if the Access-Control-Allow-Origin header has a value other
// than *.
Expand Down Expand Up @@ -307,15 +310,16 @@ class COMPONENT_EXPORT(NETWORK_CPP) CrossOriginReadBlocking {
kYes,
};

private:
CrossOriginReadBlocking(); // Not instantiable.
// Returns whether `mime_type` is a Javascript MIME type based on
// https://mimesniff.spec.whatwg.org/#javascript-mime-type
static bool IsJavascriptMimeType(base::StringPiece mime_type);

// Returns the representative mime type enum value of the mime type of
// response. For example, this returns the same value for all text/xml mime
// type families such as application/xml, application/rss+xml.
static MimeType GetCanonicalMimeType(base::StringPiece mime_type);
FRIEND_TEST_ALL_PREFIXES(CrossOriginReadBlockingTest, GetCanonicalMimeType);

private:
// Returns whether this scheme is a target of the cross-origin read blocking
// (CORB) policy. This returns true only for http://* and https://* urls.
static bool IsBlockableScheme(const GURL& frame_origin);
Expand Down
220 changes: 220 additions & 0 deletions services/network/public/cpp/opaque_response_blocking.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "services/network/public/cpp/opaque_response_blocking.h"

#include "base/metrics/histogram_functions.h"
#include "base/strings/string_piece.h"
#include "net/url_request/url_request.h"
#include "services/network/public/cpp/cross_origin_read_blocking.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/url_response_head.mojom.h"

namespace network {

namespace {

// This corresponds to "opaque-blocklisted-never-sniffed MIME type" in ORB spec.
bool IsOpaqueBlocklistedNeverSniffedMimeType(base::StringPiece mime_type) {
return CrossOriginReadBlocking::GetCanonicalMimeType(mime_type) ==
CrossOriginReadBlocking::MimeType::kNeverSniffed;
}

// ORB spec says that "An opaque-safelisted MIME type" is a JavaScript MIME type
// or a MIME type whose essence is "text/css" or "image/svg+xml".
bool IsOpaqueSafelistedMimeType(base::StringPiece mime_type) {
// Based on the spec: Is it a MIME type whose essence is "text/css" or
// "image/svg+xml"?
if (base::LowerCaseEqualsASCII(mime_type, "image/svg+xml") ||
base::LowerCaseEqualsASCII(mime_type, "text/css")) {
return true;
}

// Based on the spec: Is it a JavaScript MIME type?
if (CrossOriginReadBlocking::IsJavascriptMimeType(mime_type))
return true;

// https://github.com/annevk/orb/issues/20 tracks explicitly covering DASH
// mime type in the ORB algorithm.
if (base::LowerCaseEqualsASCII(mime_type, "application/dash+xml"))
return true;

return false;
}

// Return true for multimedia MIME types that
// 1) are not explicitly covered by ORB (e.g. that do not begin with "audio/",
// "image/", "video/" and that are not covered by
// IsOpaqueSafelistedMimeType).
// 2) would be recognized by sniffing from steps 6 or 7 of ORB:
// step 6. If the image type pattern matching algorithm ...
// step 7. If the audio or video type pattern matching algorithm ...
bool IsSniffableMultimediaType(base::StringPiece mime_type) {
if (base::LowerCaseEqualsASCII(mime_type, "application/ogg"))
return true;

return false;
}

// This corresponds to https://fetch.spec.whatwg.org/#ok-status
bool IsOkayHttpStatus(const mojom::URLResponseHead& response) {
if (!response.headers)
return false;

int code = response.headers->response_code();
return (200 <= code) && (code <= 299);
}

bool IsHttpStatus(const mojom::URLResponseHead& response,
int expected_status_code) {
if (!response.headers)
return false;

int code = response.headers->response_code();
return code == expected_status_code;
}

bool IsOpaqueResponse(const base::Optional<url::Origin>& request_initiator,
mojom::RequestMode request_mode,
const mojom::URLResponseHead& response) {
// ORB only applies to "no-cors" requests.
if (request_mode != mojom::RequestMode::kNoCors)
return false;

// Browser-initiated requests are never opaque.
if (!request_initiator.has_value())
return false;

// Requests from foo.example.com will consult foo.example.com's service worker
// first (if one has been registered). The service worker can handle requests
// initiated by foo.example.com even if they are cross-origin (e.g. requests
// for bar.example.com). This is okay, because there is no security boundary
// between foo.example.com and the service worker of foo.example.com + because
// the response data is "conjured" within the service worker of
// foo.example.com (rather than being fetched from bar.example.com).
// Therefore such responses should not be blocked by CORB, unless the
// initiator opted out of CORS / opted into receiving an opaque response. See
// also https://crbug.com/803672.
if (response.was_fetched_via_service_worker) {
switch (response.response_type) {
case network::mojom::FetchResponseType::kBasic:
case network::mojom::FetchResponseType::kCors:
case network::mojom::FetchResponseType::kDefault:
case network::mojom::FetchResponseType::kError:
// Non-opaque responses shouldn't be blocked.
return false;
case network::mojom::FetchResponseType::kOpaque:
case network::mojom::FetchResponseType::kOpaqueRedirect:
// Opaque responses are eligible for blocking. Continue on...
break;
}
}

return true;
}

ResponseHeadersHeuristicForUma CalculateResponseHeadersHeuristicForUma(
const GURL& request_url,
const base::Optional<url::Origin>& request_initiator,
mojom::RequestMode request_mode,
const mojom::URLResponseHead& response) {
// Exclude responses that ORB doesn't apply to.
if (!IsOpaqueResponse(request_initiator, request_mode, response))
return ResponseHeadersHeuristicForUma::kNonOpaqueResponse;
DCHECK(request_initiator.has_value());

// Same-origin requests are allowed (the spec doesn't explicitly deal with
// this).
url::Origin target_origin = url::Origin::Create(request_url);
if (request_initiator->IsSameOriginWith(target_origin))
return ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders;

// Presence of an "X-Content-Type-Options: nosniff" header means that ORB will
// reach a final decision in step 8, before reaching Javascript parsing in
// step 12:
// step 8. If nosniff is true, then return false.
// ...
// step 12. If response's body parses as JavaScript ...
if (CrossOriginReadBlocking::ResponseAnalyzer::HasNoSniff(response))
return ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders;

// If a mime type is missing then ORB will reach a final decision in step 10,
// before reaching Javascript parsing in step 12:
// step 10. If mimeType is failure, then return true.
// ...
// step 12. If response's body parses as JavaScript ...
std::string mime_type;
if (!response.headers || !response.headers->GetMimeType(&mime_type))
return ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders;

// Specific MIME types might make ORB reach a final decision before reaching
// Javascript parsing step:
// step 3.i. If mimeType is an opaque-safelisted MIME type, then return
// true.
// step 3.ii. If mimeType is an opaque-blocklisted-never-sniffed MIME
// type, then return false.
// ...
// step 11. If mimeType's essence starts with "audio/", "image/", or
// "video/", then return false.
// ...
// step 12. If response's body parses as JavaScript ...
if (IsOpaqueBlocklistedNeverSniffedMimeType(mime_type) ||
IsOpaqueSafelistedMimeType(mime_type) ||
IsSniffableMultimediaType(mime_type)) {
return ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders;
}
constexpr auto kCaseInsensitive = base::CompareCase::INSENSITIVE_ASCII;
if (base::StartsWith(mime_type, "audio/", kCaseInsensitive) ||
base::StartsWith(mime_type, "image/", kCaseInsensitive) ||
base::StartsWith(mime_type, "video/", kCaseInsensitive)) {
return ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders;
}

// If the http response indicates an error, or a 206 response, then ORB will
// reach a final decision before reaching Javascript parsing in step 12:
// step 9. If response's status is not an ok status, then return false.
// ...
// step 12. If response's body parses as JavaScript ...
if (!IsOkayHttpStatus(response) || IsHttpStatus(response, 206))
return ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders;

// Otherwise we need to parse the response body as Javascript.
return ResponseHeadersHeuristicForUma::kRequiresJavascriptParsing;
}

} // namespace

void LogUmaForOpaqueResponseBlocking(
const GURL& request_url,
const base::Optional<url::Origin>& request_initiator,
mojom::RequestMode request_mode,
mojom::RequestDestination request_destination,
const mojom::URLResponseHead& response) {
ResponseHeadersHeuristicForUma response_headers_decision =
CalculateResponseHeadersHeuristicForUma(request_url, request_initiator,
request_mode, response);
base::UmaHistogramEnumeration(
"SiteIsolation.ORB.ResponseHeadersHeuristic.Decision",
response_headers_decision);

switch (response_headers_decision) {
case ResponseHeadersHeuristicForUma::kNonOpaqueResponse:
break;

case ResponseHeadersHeuristicForUma::kProcessedBasedOnHeaders:
base::UmaHistogramEnumeration(
"SiteIsolation.ORB.ResponseHeadersHeuristic.ProcessedBasedOnHeaders",
request_destination);
break;

case ResponseHeadersHeuristicForUma::kRequiresJavascriptParsing:
base::UmaHistogramEnumeration(
"SiteIsolation.ORB.ResponseHeadersHeuristic."
"RequiresJavascriptParsing",
request_destination);
break;
}
}

} // namespace network

0 comments on commit 600fb32

Please sign in to comment.