Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Media
DescriptionInappropriate implementation in Media
ComponentMedia
Bug ClassLogic Error
Tracker513054053
Fix commit6d0f6d57d3c9 (chromium/src) +42/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Files Changed

  • media/formats/hls/media_playlist.cc
  • media/formats/hls/media_playlist_unittest.cc
From 6d0f6d57d3c96c0c925e28c8662fc2ad8c34065f Mon Sep 17 00:00:00 2001
From: Ted Meyer <[email protected]>
Date: Mon, 18 May 2026 17:56:43 -0700
Subject: [PATCH] HLS: Check matching security origin for key

Turns out there are _lots_ of weird ways to format a URI that are
acceptable, including leading backslashes, spaces, and more. By taking
the resolved URI and getting its security origin and comparing that to
the playlist URI, we can use the much better tested security boundaries
that exist already in chrome.

Fixed: 513054053
Change-Id: If61953e58046bf58e66e464f4e9daf844a33c3e6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7857378
Reviewed-by: Dale Curtis <[email protected]>
Commit-Queue: Ted (Chromium) Meyer <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1632542}
---

diff --git a/media/formats/hls/media_playlist.cc b/media/formats/hls/media_playlist.cc
index 69edb79..b3b7e4d1 100644
--- a/media/formats/hls/media_playlist.cc
+++ b/media/formats/hls/media_playlist.cc
@@ -25,6 +25,7 @@
 #include "media/formats/hls/types.h"
 #include "media/formats/hls/variable_dictionary.h"
 #include "url/gurl.h"
+#include "url/origin.h"
 
 namespace media::hls {
 
@@ -252,12 +253,11 @@
               // be populated by an opaque fetch response.
               key_location =
                   MediaSegment::EncryptionData::KeyLocation::kSafeOrigin;
-            } else if (!declared_uri_value.starts_with("//") &&
-                       !GURL(declared_uri_value).has_scheme()) {
-              // "Path-only" URLs are considered safe as well, since they are
-              // hosted on the same origin as the manifest in which they are
-              // declared. Note that this _not_ the same as the page security
-              // origin.
+            } else if (url::Origin::Create(resource_uri)
+                           .IsSameOriginWith(
+                               url::Origin::Create(playlist_uri))) {
+              // Same-origin URLs (including resolved path-only URLs) are
+              // considered safe as well.
               key_location =
                   MediaSegment::EncryptionData::KeyLocation::kSafeOrigin;
             }
diff --git a/media/formats/hls/media_playlist_unittest.cc b/media/formats/hls/media_playlist_unittest.cc
index a8f97424..f03b887 100644
--- a/media/formats/hls/media_playlist_unittest.cc
+++ b/media/formats/hls/media_playlist_unittest.cc
@@ -1629,6 +1629,42 @@
                       std::make_tuple(0, 10),
                       MediaSegment::EncryptionData::KeyLocation::kSafeOrigin));
   builder.ExpectOk();
+
+  // Test backslash bypass (should be classified as UnsafeOrigin because it
+  // resolves to cross-origin)
+  builder.AppendLine(
+      "#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI=\"/\\\\victim.com/key\","
+      "KEYFORMAT=\"identity\"");
+  builder.AppendLine("#EXTINF:1.60000,");
+  builder.AppendLine("data09.ts");
+  builder.ExpectAdditionalSegment();
+  builder.ExpectSegment(HasUri, GURL("http://localhost/data09.ts"));
+  builder.ExpectSegment(HasMediaSequenceNumber, 11);
+  builder.ExpectSegment(
+      HasEncryptionData,
+      std::make_tuple(
+          GURL("http://victim.com/key"), XKeyTagMethod::kSampleAESCTR,
+          XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 11),
+          MediaSegment::EncryptionData::KeyLocation::kUnsafeOrigin));
+  builder.ExpectOk();
+
+  // Test leading whitespace bypass (should be classified as UnsafeOrigin
+  // because it resolves to cross-origin)
+  builder.AppendLine(
+      "#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI=\"  //victim.com/key\","
+      "KEYFORMAT=\"identity\"");
+  builder.AppendLine("#EXTINF:1.60000,");
+  builder.AppendLine("data10.ts");
+  builder.ExpectAdditionalSegment();
+  builder.ExpectSegment(HasUri, GURL("http://localhost/data10.ts"));
+  builder.ExpectSegment(HasMediaSequenceNumber, 12);
+  builder.ExpectSegment(
+      HasEncryptionData,
+      std::make_tuple(
+          GURL("http://victim.com/key"), XKeyTagMethod::kSampleAESCTR,
+          XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 12),
+          MediaSegment::EncryptionData::KeyLocation::kUnsafeOrigin));
+  builder.ExpectOk();
 }
 
 }  // namespace media::hls
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/formats/hls/media_playlist_unittest.cc b/media/formats/hls/media_playlist_unittest.cc
index a8f97424..f03b887 100644
--- a/media/formats/hls/media_playlist_unittest.cc
+++ b/media/formats/hls/media_playlist_unittest.cc
@@ -1629,6 +1629,42 @@
                       std::make_tuple(0, 10),
                       MediaSegment::EncryptionData::KeyLocation::kSafeOrigin));
   builder.ExpectOk();
+
+  // Test backslash bypass (should be classified as UnsafeOrigin because it
+  // resolves to cross-origin)
+  builder.AppendLine(
+      "#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI=\"/\\\\victim.com/key\","
+      "KEYFORMAT=\"identity\"");
+  builder.AppendLine("#EXTINF:1.60000,");
+  builder.AppendLine("data09.ts");
+  builder.ExpectAdditionalSegment();
+  builder.ExpectSegment(HasUri, GURL("http://localhost/data09.ts"));
+  builder.ExpectSegment(HasMediaSequenceNumber, 11);
+  builder.ExpectSegment(
+      HasEncryptionData,
+      std::make_tuple(
+          GURL("http://victim.com/key"), XKeyTagMethod::kSampleAESCTR,
+          XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 11),
+          MediaSegment::EncryptionData::KeyLocation::kUnsafeOrigin));
+  builder.ExpectOk();
+
+  // Test leading whitespace bypass (should be classified as UnsafeOrigin
+  // because it resolves to cross-origin)
+  builder.AppendLine(
+      "#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI=\"  //victim.com/key\","
+      "KEYFORMAT=\"identity\"");
+  builder.AppendLine("#EXTINF:1.60000,");
+  builder.AppendLine("data10.ts");
+  builder.ExpectAdditionalSegment();
+  builder.ExpectSegment(HasUri, GURL("http://localhost/data10.ts"));
+  builder.ExpectSegment(HasMediaSequenceNumber, 12);
+  builder.ExpectSegment(
+      HasEncryptionData,
+      std::make_tuple(
+          GURL("http://victim.com/key"), XKeyTagMethod::kSampleAESCTR,
+          XKeyTagKeyFormat::kIdentity, std::make_tuple(0, 12),
+          MediaSegment::EncryptionData::KeyLocation::kUnsafeOrigin));
+  builder.ExpectOk();
 }
 
 }  // namespace media::hls
Loading diff…

Original Bug Report

reported by [email protected]

Same-Origin Policy bypass in HLS demuxer via URL parser differential

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A URL parser differential in the HLS demuxer allows cross-origin decryption keys to be incorrectly classified as same-origin. This bypasses security checks intended to prevent cross-origin key oracle attacks, potentially allowing an attacker to leak sensitive data from other origins.

Affected files:

  • media/formats/hls/media_playlist.cc
  • media/filters/hls_network_access_impl.cc
  • media/formats/hls/tags.cc
  • media/formats/hls/types.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

The HLS demuxer implementation in Chromium contains a logic flaw in how it classifies the origin of decryption keys specified in #EXT-X-KEY tags. Specifically, the check used to identify same-origin (path-only) URLs is inconsistent with how GURL::Resolve actually resolves those URLs. This inconsistency can be exploited to trick the demuxer into using a cross-origin resource as an AES decryption key without triggering the required security errors.

Root Cause Analysis

In media/formats/hls/media_playlist.cc, a key URI is classified as kSafeOrigin (safe to use even if the stream becomes tainted) if it does not start with // and does not have a scheme:

// media/formats/hls/media_playlist.cc:255
} else if (!declared_uri_value.starts_with("//") &&
           !GURL(declared_uri_value).has_scheme()) {
  key_location = MediaSegment::EncryptionData::KeyLocation::kSafeOrigin;
}

There are at least two ways to bypass this check on non-Windows platforms (Linux, macOS, Android, ChromeOS):

  1. Backslash Differential: A URI starting with /\ (e.g., URI="/\\victim.com/api/token").

    • The starts_with("//") check fails.
    • GURL("/\\victim.com/...").has_scheme() returns false because it lacks a colon and is invalid as an absolute URL.
    • However, GURL::Resolve (via url::ResolveRelative) treats both / and \ as slashes when identifying protocol-relative URLs. It resolves this to https://victim.com/api/token.
  2. Leading Whitespace: A URI starting with whitespace (e.g., URI=" //victim.com/token").

    • The starts_with("//") check fails.
    • GURL trims leading whitespace during resolution but not necessarily in the string comparison above, leading to a similar differential.

Exploitation Potential (Suggested Steps)

An attacker could potentially use this to leak cross-origin data (e.g., an authentication token or CSRF secret) from a site where the user is logged in:

  1. Host a malicious HLS playlist on https://attacker.com with a key URI like "/\\victim.com/api/token".
  2. When the user loads the playlist, the demuxer incorrectly marks the key as kSafeOrigin but fetches it from victim.com with the user’s cookies.
  3. In media/filters/hls_network_access_impl.cc:62, the security check if (stream->would_taint_origin() && (stream->DidRedirect() || !safe_key)) is bypassed because safe_key is true.
  4. The cross-origin response is imported as the AES-128 key.
  5. The attacker provides media segments that only decode correctly if certain guesses about the key (the victim’s data) are correct, effectively using the player as a decryption oracle to leak the data.

Note: On Windows, GURL often identifies leading \ as a UNC path and assigns a file: scheme, which may mitigate the backslash variant but potentially not the whitespace variant.

Suggested Fix

Instead of performing string-based heuristic checks on the raw declared_uri_value, the demuxer should compare the origin of the resolved resource_uri with the origin of the playlist_uri. If they do not match, the key should be marked as kUnsafeOrigin regardless of the input string’s format.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker