CVE-2026-16400
Overview
Background
- Reporting API
- a web-observable mechanism that delivers structured reports (deprecation, feature-policy violations) to page script via
ReportingObserver. - `JSCallingLocation`
- a Gecko structure capturing the JS source file, line, and column of the caller, whose
FileName()can be a full URL. - Exposable URI
- a form of
nsIURIwith sensitive parts such as embeddedusername:passwordcredentials removed, produced viansIOService::CreateExposableURI. - `ReportingUtils::StripURL`
- the canonical helper that converts an
nsIURIinto a credential-stripped string safe to surface to web content.
Root Cause Analysis
When Gecko generated a deprecation report in nsContentUtils::ReportDeprecation or a feature-policy violation report in FeaturePolicyUtils::ReportViolation, it populated the report body’s source-file field directly from aLocation.FileName() (respectively loc.FileName()), the raw JS calling-location URL. Although the top-level report url/spec was anonymized (via AnonymizeURI or CreateExposableURI), the sourceFile field bypassed that stripping entirely, so a location URL containing embedded credentials was copied verbatim into a web-observable DeprecationReportBody / FeaturePolicyViolationReportBody. The violated invariant is that every URL exposed through the Reporting API must be sanitized of credentials before reaching page script.
The fix adds ReportingUtils::StripLocationFileName, which resolves the location to an nsIURI and runs it through the same StripURL sanitizer, and both call sites now pass the stripped sourceFile instead of the raw FileName(). This closes the leak by ensuring the source-file field receives the identical credential-removing treatment already applied to the report URL.
FileName() into the report body unfiltered; the fix routes that filename through the same StripURL path so both fields are credential-stripped consistently.Attack Path
- Embed credentials in a script URL
An attacker causes a document to load or reference script from a URL containing
username:password@credentials. - Trigger a report from that script Script at the credentialed URL invokes a deprecated API or violates a feature policy, making its URL the JS calling location.
- Leak via the source-file field
Gecko builds the report body with the raw
FileName(), so the credentialed URL lands in the observablesourceFilefield. - Observe the report
Page script registers a
ReportingObserverand reads back the report body, recovering the unsanitized URL including its credentials.
Impact Assessment
username:password credentials) that were meant to be stripped before crossing into web-observable content, all within the content process via the Reporting API. Preconditions are that a report-generating code path attributes the event to a calling-location URL carrying such sensitive parts, and that script can observe the resulting report. This is an information-disclosure (moderate) issue, not memory corruption or code execution.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/base/nsContentUtils.cpp |
modified | |
ifdom/reporting/ReportingUtils.cpp |
modified | |
nsAtomdom/reporting/ReportingUtils.h |
modified | |
ReportingUtilsdom/reporting/ReportingUtils.h |
modified | |
ifdom/security/featurepolicy/FeaturePolicyUtils.cpp |
modified |
Files Changed
dom/base/nsContentUtils.cppdom/reporting/ReportingUtils.cppdom/reporting/ReportingUtils.hdom/security/featurepolicy/FeaturePolicyUtils.cpp
Audit Directions
- Report-body fieldsAudit every
ReportBodysubclass construction for URL-bearing fields (source file, endpoints, referrers) that bypassStripURL/exposable-URI sanitization. - Raw `FileName()` usageSearch for direct uses of
JSCallingLocation::FileName()orSourceLocationfilenames flowing into web-observable output without credential stripping. - Partial sanitizationFlag code paths that sanitize a primary URL but copy a secondary, related URL (from the same location or resource) unstripped into the same surfaced structure.
Patch
diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp
index 264f64c35b2..4600cfde9c4 100644
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -5522,14 +5522,8 @@ void nsContentUtils::ReportDeprecation(
MOZ_ASSERT(aGlobal);
MOZ_ASSERT(aURI);
- // If the URI has the data scheme, report that instead of the spec,
- // as the spec may be arbitrarily long and we would like to avoid
- // copying it.
- nsAutoCString specOrScheme;
- nsresult rv = nsContentUtils::AnonymizeURI(aURI, specOrScheme);
- if (NS_WARN_IF(NS_FAILED(rv))) {
- return;
- }
+ nsAutoCString url;
+ ReportingUtils::StripURL(aURI, url);
const char* operation =
kDeprecatedOperations[static_cast<size_t>(aOperation)];
@@ -5542,25 +5536,27 @@ void nsContentUtils::ReportDeprecation(
// XXX do we really want the localized string for deprecation report?
nsAutoString msg;
- rv = nsContentUtils::GetMaybeLocalizedString(PropertiesFile::DOM_PROPERTIES,
- key.get(), aDoc, msg);
+ nsresult rv = nsContentUtils::GetMaybeLocalizedString(
+ PropertiesFile::DOM_PROPERTIES, key.get(), aDoc, msg);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
Nullable<uint32_t> lineNumber;
Nullable<uint32_t> columnNumber;
+ nsAutoCString sourceFile;
if (aLocation) {
lineNumber.SetValue(aLocation.mLine);
columnNumber.SetValue(aLocation.mColumn);
+ ReportingUtils::StripLocationFileName(aLocation, sourceFile);
}
RefPtr<DeprecationReportBody> body =
new DeprecationReportBody(aGlobal, type, nullptr /* date */, msg,
- aLocation.FileName(), lineNumber, columnNumber);
+ sourceFile, lineNumber, columnNumber);
ReportingUtils::Report(aGlobal, nsGkAtoms::deprecation, u"default"_ns,
- NS_ConvertUTF8toUTF16(specOrScheme), body);
+ NS_ConvertUTF8toUTF16(url), body);
}
void nsContentUtils::LogMessageToConsole(const char* aMsg) {
diff --git a/dom/reporting/ReportingUtils.cpp b/dom/reporting/ReportingUtils.cpp
index 4a481617108..9db668720c7 100644
--- a/dom/reporting/ReportingUtils.cpp
+++ b/dom/reporting/ReportingUtils.cpp
@@ -42,6 +42,22 @@ void ReportingUtils::StripURL(nsIURI* aURI, nsACString& outStrippedURL) {
stripped->GetSpec(outStrippedURL);
}
+// static
+void ReportingUtils::StripLocationFileName(
+ const mozilla::JSCallingLocation& aLocation,
+ nsACString& outStrippedFileName) {
+ nsCOMPtr<nsIURI> uri;
+ if (aLocation.mResource.is<nsCOMPtr<nsIURI>>()) {
+ uri = aLocation.mResource.as<nsCOMPtr<nsIURI>>();
+ } else {
+ (void)NS_NewURI(getter_AddRefs(uri), aLocation.FileName());
+ }
+
+ if (uri) {
+ ReportingUtils::StripURL(uri, outStrippedFileName);
+ }
+}
+
// static
void ReportingUtils::Report(nsIGlobalObject* aGlobal, nsAtom* aType,
const nsAString& aGroupName, const nsAString& aURL,
diff --git a/dom/reporting/ReportingUtils.h b/dom/reporting/ReportingUtils.h
index c6aba30112b..752f313ce13 100644
--- a/dom/reporting/ReportingUtils.h
+++ b/dom/reporting/ReportingUtils.h
@@ -5,6 +5,7 @@
#ifndef mozilla_dom_ReportingUtils_h
#define mozilla_dom_ReportingUtils_h
+#include "mozilla/SourceLocation.h"
#include "nsString.h"
class nsAtom;
@@ -19,6 +20,8 @@ class ReportBody;
class ReportingUtils final {
public:
static void StripURL(nsIURI* aURI, nsACString& outStrippedURL);
+ static void StripLocationFileName(const mozilla::JSCallingLocation& aLocation,
+ nsACString& outStrippedFileName);
static void Report(nsIGlobalObject* aGlobal, nsAtom* aType,
const nsAString& aGroupName, const nsAString& aURL,
diff --git a/dom/security/featurepolicy/FeaturePolicyUtils.cpp b/dom/security/featurepolicy/FeaturePolicyUtils.cpp
index f8208bfed77..656117208c1 100644
--- a/dom/security/featurepolicy/FeaturePolicyUtils.cpp
+++ b/dom/security/featurepolicy/FeaturePolicyUtils.cpp
@@ -215,14 +215,9 @@ void FeaturePolicyUtils::ReportViolation(Document* aDocument,
return;
}
- // Strip the URL of any possible username/password and make it ready to be
- // presented in the UI.
- nsCOMPtr<nsIURI> exposableURI = net::nsIOService::CreateExposableURI(uri);
- nsAutoCString spec;
- nsresult rv = exposableURI->GetSpec(spec);
- if (NS_WARN_IF(NS_FAILED(rv))) {
- return;
- }
+ nsAutoCString url;
+ ReportingUtils::StripURL(uri, url);
+
JSContext* cx = nsContentUtils::GetCurrentJSContext();
if (NS_WARN_IF(!cx)) {
return;
@@ -230,10 +225,11 @@ void FeaturePolicyUtils::ReportViolation(Document* aDocument,
Nullable<int32_t> lineNumber;
Nullable<int32_t> columnNumber;
- auto loc = JSCallingLocation::Get();
- if (loc) {
+ nsAutoCString sourceFile;
+ if (auto loc = JSCallingLocation::Get()) {
lineNumber.SetValue(static_cast<int32_t>(loc.mLine));
columnNumber.SetValue(static_cast<int32_t>(loc.mColumn));
+ ReportingUtils::StripLocationFileName(loc, sourceFile);
}
nsPIDOMWindowInner* window = aDocument->GetInnerWindow();
@@ -243,11 +239,11 @@ void FeaturePolicyUtils::ReportViolation(Document* aDocument,
RefPtr<FeaturePolicyViolationReportBody> body =
new FeaturePolicyViolationReportBody(window->AsGlobal(), aFeatureName,
- loc.FileName(), lineNumber,
- columnNumber, u"enforce"_ns);
+ sourceFile, lineNumber, columnNumber,
+ u"enforce"_ns);
ReportingUtils::Report(window->AsGlobal(), nsGkAtoms::featurePolicyViolation,
- u"default"_ns, NS_ConvertUTF8toUTF16(spec), body);
+ u"default"_ns, NS_ConvertUTF8toUTF16(url), body);
}
} // namespace dom