Source code
Revision control
Copy as Markdown
Other Tools
/* 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
#include "ConnectionAllowlists.h"
#include <functional>
#include <utility>
#include "mozilla/Logging.h"
#include "mozilla/StaticPrefs_security.h"
#include "mozilla/dom/ConnectionAllowlistViolationReportBody.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/ReportingUtils.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "mozilla/net/SFV.h"
#include "mozilla/net/URLPatternGlue.h"
#include "nsGlobalWindowInner.h"
#include "nsIGlobalObject.h"
#include "nsILoadInfo.h"
#include "nsNetUtil.h"
#include "nsScriptSecurityManager.h"
#include "nsString.h"
using namespace mozilla;
static LazyLogModule sConnectionAllowlistsLog("ConnectionAllowlists");
#define LOG(fmt, ...) \
MOZ_LOG_FMT(sConnectionAllowlistsLog, LogLevel::Debug, fmt, ##__VA_ARGS__)
namespace mozilla::dom {
void ConnectionAllowlists::Allowlist::AppendPattern(
const nsACString& aSerializedPattern) {
UrlPatternGlue pattern = nullptr;
UrlPatternOptions options{};
if (!urlpattern_parse_pattern_from_string(&aSerializedPattern, nullptr,
options, &pattern)) {
LOG("Failed to parse URLPattern: {}", aSerializedPattern);
return;
}
mPatterns.AppendElement(UrlPattern(std::move(pattern)));
mSerializedPatterns.AppendElement(aSerializedPattern);
}
/* static */
Maybe<ConnectionAllowlists::Allowlist>
ConnectionAllowlists::ParseConnectionAllowlistHeader(const nsACString& aHeader,
Disposition aDisposition) {
if (aHeader.IsEmpty()) {
return Nothing();
}
// 1. If list's size is 0, return null.
auto list = net::SFV::ParseList(aHeader);
if (!list.IsValid() || list.Length() == 0) {
LOG("Failed to parse header as a structured field list.");
return Nothing();
}
// 2. If list[0] is not an inner list, return null.
auto innerList = list.GetInnerListAt(0);
if (!innerList.IsValid()) {
LOG("First list member is not an inner list.");
return Nothing();
}
// 3. Let allowlist be a Connection Allowlist whose disposition is
// disposition.
ConnectionAllowlists::Allowlist allowlist;
allowlist.mDisposition = aDisposition;
// 4. For each item in list[0]:
size_t length = innerList.Length();
for (size_t i = 0; i < length; i++) {
auto item = innerList.GetItemAt(i);
if (!item.IsValid()) {
continue;
}
// 4.1. Let serialized pattern be null.
Maybe<nsAutoCString> serializedPattern;
nsAutoCString token;
nsAutoCString string;
if (NS_SUCCEEDED(item.GetValue<net::SFV::Token>(token))) {
// 4.2. If item is the token `response-origin`:
// 4.2.1. Set serialized pattern to the ASCII serialization of
// response-url's origin.
//
// Unlike the spec we remember the token instead of resolving it here,
// see the comment on mMatchesResponseOrigin.
if (token.EqualsLiteral("response-origin")) {
allowlist.mMatchesResponseOrigin = true;
}
} else if (NS_SUCCEEDED(item.GetValue<net::SFV::SFVString>(string))) {
// 4.3. If item is a string, set serialized pattern to item.
serializedPattern.emplace(string);
}
// 4.4. If serialized pattern is null, continue.
if (serializedPattern.isNothing()) {
continue;
}
// 4.5. Let URL pattern be the result of executing build a URL pattern from
// an HTTP structured field value given serialized pattern with null as the
// base URL. If this step throws an error, continue.
//
// 4.6. Append URL pattern to allowlist's allowlist.
allowlist.AppendPattern(*serializedPattern);
}
// 5. For each key → value in list[0]'s parameters:
nsAutoCString paramValue;
// 5.1. If key is `report-to` and value is a token, set allowlist's reporting
// endpoint to value.
if (NS_SUCCEEDED(
innerList.GetParam<net::SFV::Token>("report-to"_ns, paramValue))) {
allowlist.mReportingEndpoint = paramValue;
}
// 5.2. If key is `redirects` and value is a token: If value is `block`, set
// redirects to block. Else, set redirects to allow.
if (NS_SUCCEEDED(
innerList.GetParam<net::SFV::Token>("redirects"_ns, paramValue))) {
allowlist.mRedirects = paramValue.EqualsLiteral("block")
? ConnectionAllowlists::Redirects::Block
: ConnectionAllowlists::Redirects::Allow;
}
// 5.3. If key is `webrtc` and value is a token: If value is `block`, set
// webrtc to block. Else, set webrtc to allow.
if (NS_SUCCEEDED(
innerList.GetParam<net::SFV::Token>("webrtc"_ns, paramValue))) {
allowlist.mWebRTC = paramValue.EqualsLiteral("block")
? ConnectionAllowlists::WebRTC::Block
: ConnectionAllowlists::WebRTC::Allow;
}
// 6. Return allowlist.
return Some(std::move(allowlist));
}
/* static */
nsresult ConnectionAllowlists::ParseHeaders(const nsACString& aHeader,
const nsACString& aReportOnlyHeader,
ConnectionAllowlists** aResult) {
*aResult = nullptr;
if (!StaticPrefs::security_connection_allowlists_enabled()) {
return NS_OK;
}
Maybe<Allowlist> enforcement =
ParseConnectionAllowlistHeader(aHeader, Disposition::Enforce);
Maybe<Allowlist> reportOnly =
ParseConnectionAllowlistHeader(aReportOnlyHeader, Disposition::Report);
if (enforcement.isNothing() && reportOnly.isNothing()) {
return NS_OK;
}
RefPtr<ConnectionAllowlists> allowlists = new ConnectionAllowlists();
allowlists->mEnforcement = std::move(enforcement);
allowlists->mReportOnly = std::move(reportOnly);
allowlists.forget(aResult);
return NS_OK;
}
void ConnectionAllowlists::SetResponseURI(nsIURI* aURI) {
MOZ_ASSERT(!mFrozen);
mResponseURI = aURI;
}
bool ConnectionAllowlists::ShouldLoad(nsIURI* aURI,
nsILoadInfo* aLoadInfo) const {
// TODO: We probably need to exempt some content like in SubjectToCSP.
// TODO: Requests vs URL.
return !ShouldBlockURL(aURI, aLoadInfo);
}
// https://wicg.github.io/connection-allowlists/#abstract-opdef-match-a-url-to-a-connection-allowlist
bool ConnectionAllowlists::MatchURL(nsIURI* aURI,
const Allowlist& aAllowlist) const {
// 1. If url is local, return success.
// TODO: helper? add chrome:?
if (aURI->SchemeIs("about") || aURI->SchemeIs("data") ||
aURI->SchemeIs("blob")) {
return true;
}
// If list included the `response-origin` token, then allow all same-origin
// URLs.
if (aAllowlist.mMatchesResponseOrigin && mResponseURI) {
if (nsScriptSecurityManager::SecurityCompareURIs(mResponseURI, aURI)) {
return true;
}
}
// XXX This seems like something we should have abstracted.
nsAutoCString spec;
if (NS_WARN_IF(NS_FAILED(aURI->GetSpec(spec)))) {
return false;
}
UrlPatternInput input = net::CreateUrlPatternInput(spec);
// 2. For each pattern in connection allowlist’s allowlist:
for (const auto& pattern : aAllowlist.mPatterns) {
// 2.1. If URL pattern matching given pattern and url does not return null,
// return success.
if (net::UrlPatternTest(pattern.get(), input, Nothing())) {
return true;
}
}
// 3. Return failure.
return false;
}
bool ConnectionAllowlists::ShouldBlockURL(nsIURI* aURI,
nsILoadInfo* aLoadInfo) const {
// 1. For each connection allowlist in connection allowlists:
for (const Maybe<Allowlist>& allowlist :
{std::cref(mEnforcement), std::cref(mReportOnly)}) {
if (allowlist.isNothing()) {
continue;
}
// 1.1. If url matches connection allowlist, continue.
if (MatchURL(aURI, *allowlist)) {
continue;
}
// 1.2. Report a violation given url, environment, and connection
// allowlist.
ReportViolation(AsVariant(aURI), aLoadInfo, *allowlist);
// 1.3. If connection allowlist's disposition is enforce, return blocked.
if (allowlist->mDisposition == Disposition::Enforce) {
LOG("Blocking URL: {}", aURI->GetSpecOrDefault());
return true;
}
}
// 2. Return allowed.
return false;
}
/* static */
void ConnectionAllowlists::ReportViolation(
const Variant<nsIURI*, nsCString>& aResource, nsILoadInfo* aLoadInfo,
const Allowlist& aAllowlist) {
// 1. If allowlist’s reporting endpoint is null, return.
if (aAllowlist.mReportingEndpoint.IsEmpty()) {
return;
}
// The report is queued on the environment that initiated the load.
// TODO: This will fail when blocking in the parent process for e.g.
// navigation.
RefPtr<nsGlobalWindowInner> window =
nsGlobalWindowInner::GetInnerWindowWithId(aLoadInfo->GetInnerWindowID());
if (!window) {
LOG("Not reporting a violation, no global for the load.");
return;
}
Document* doc = window->GetExtantDoc();
if (NS_WARN_IF(!doc) || NS_WARN_IF(!doc->GetDocumentURI())) {
return;
}
// 2. Let violation be a new ConnectionAllowlistViolationReport, initialized
// as follows:
//
// url
// environment’s creation URL, stripped for use in reports.
nsAutoCString url;
ReportingUtils::StripURL(doc->GetDocumentURI(), url);
// connection
// If resource URL is a URL, then resource URL, stripped for use in reports.
// Otherwise, resource URL.
nsAutoCString connection;
if (aResource.is<nsIURI*>()) {
nsCOMPtr<nsIURI> uri = aResource.as<nsIURI*>();
ReportingUtils::StripURL(uri, connection);
} else {
connection = aResource.as<nsCString>();
}
// allowlist
// A new list containing the result of serializing each pattern in
// allowlist’s allowlist
//
// disposition
// allowlist’s disposition.
RefPtr<ConnectionAllowlistViolationReportBody> violation =
new ConnectionAllowlistViolationReportBody(
window, url, connection, aAllowlist.mSerializedPatterns.Clone(),
aAllowlist.mDisposition == Disposition::Enforce
? ConnectionAllowlistDisposition::Enforce
: ConnectionAllowlistDisposition::Report);
// 3. Generate and queue a report given environment as the context,
// "connection-allowlist" as the type, allowlist’s reporting endpoint as the
// destination, and violation as the data.
ReportingUtils::Report(window, nsGkAtoms::connection_allowlist,
aAllowlist.mReportingEndpoint, url, violation);
}
void ConnectionAllowlists::Allowlist::ToEntryArgs(
mozilla::ipc::ConnectionAllowlistEntry& aEntry) const {
aEntry.patterns() = mSerializedPatterns.Clone();
aEntry.matchesResponseOrigin() = mMatchesResponseOrigin;
aEntry.reportingEndpoint() = mReportingEndpoint;
aEntry.allowRedirects() = mRedirects == Redirects::Allow;
aEntry.allowWebRTC() = mWebRTC == WebRTC::Allow;
}
/* static */
ConnectionAllowlists::Allowlist ConnectionAllowlists::Allowlist::FromEntryArgs(
const mozilla::ipc::ConnectionAllowlistEntry& aEntry,
Disposition aDisposition) {
Allowlist allowlist;
allowlist.mDisposition = aDisposition;
for (const nsCString& serializedPattern : aEntry.patterns()) {
allowlist.AppendPattern(serializedPattern);
}
allowlist.mMatchesResponseOrigin = aEntry.matchesResponseOrigin();
allowlist.mReportingEndpoint = aEntry.reportingEndpoint();
allowlist.mRedirects =
aEntry.allowRedirects() ? Redirects::Allow : Redirects::Block;
allowlist.mWebRTC = aEntry.allowWebRTC() ? WebRTC::Allow : WebRTC::Block;
return allowlist;
}
void ConnectionAllowlists::ToArgs(
mozilla::ipc::ConnectionAllowlistsArgs& aArgs) const {
aArgs.enforcement() = Nothing();
aArgs.reportOnly() = Nothing();
aArgs.responseURISpec().Truncate();
if (mEnforcement) {
mozilla::ipc::ConnectionAllowlistEntry entry;
mEnforcement->ToEntryArgs(entry);
aArgs.enforcement() = Some(std::move(entry));
}
if (mReportOnly) {
mozilla::ipc::ConnectionAllowlistEntry entry;
mReportOnly->ToEntryArgs(entry);
aArgs.reportOnly() = Some(std::move(entry));
}
if (mResponseURI &&
NS_WARN_IF(NS_FAILED(mResponseURI->GetSpec(aArgs.responseURISpec())))) {
aArgs.responseURISpec().Truncate();
}
}
/* static */
already_AddRefed<ConnectionAllowlists> ConnectionAllowlists::FromArgs(
const mozilla::ipc::ConnectionAllowlistsArgs& aArgs) {
if (aArgs.enforcement().isNothing() && aArgs.reportOnly().isNothing()) {
return nullptr;
}
RefPtr<ConnectionAllowlists> allowlists = new ConnectionAllowlists();
if (aArgs.enforcement().isSome()) {
allowlists->mEnforcement.emplace(
Allowlist::FromEntryArgs(*aArgs.enforcement(), Disposition::Enforce));
}
if (aArgs.reportOnly().isSome()) {
allowlists->mReportOnly.emplace(
Allowlist::FromEntryArgs(*aArgs.reportOnly(), Disposition::Report));
}
if (!aArgs.responseURISpec().IsEmpty()) {
nsCOMPtr<nsIURI> responseURI;
if (NS_SUCCEEDED(
NS_NewURI(getter_AddRefs(responseURI), aArgs.responseURISpec()))) {
allowlists->mResponseURI = std::move(responseURI);
} else {
LOG("Failed to parse responseURISpec: {}", aArgs.responseURISpec());
}
}
return allowlists.forget();
}
} // namespace mozilla::dom
#undef LOG