Medium CVSS 8.1 webkit Cross Origin 🔧 Commit mapped

Overview

Medium
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionA malicious website may exfiltrate data cross-origin
ComponentWebCore
Sub-componentWebCore WebGPU
Bug ClassCross Origin
Tracker313357
Fix commit8254b44eba7e (WebKit/WebKit) +103/-3
CWECWE-352 (CSRF)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N
CISA KEVNot listed
CreditedRhyru9, Merrick Hare, Kwak Kiyong, Song Nuri, Khai Tran, John Lussier, Gurpreet Shergill, Drinor Selmanaj (Sentry)
Disclosed2026-06-29

Background

Same-Origin Policy (SOP)
The core web isolation rule preventing one origin from reading another origin’s content without permission (CORS).
Origin taint
Media loaded cross-origin without CORS is ’tainted’; the pixels may be displayed but must not be read back. Canvas enforces this by throwing on getImageData().
importExternalTexture
A WebGPU API that ingests an HTMLVideoElement’s frames as a texture usable in shaders — a path that, if unchecked, lets shader output be read back to script.

Root Cause Analysis

This fixes a cross-origin data leak in WebGPU’s importExternalTexture.

Before the fix, GPUDevice::importExternalTexture accepted an HTMLVideoElement source without checking whether the video is origin-tainted (cross-origin without CORS). A page could therefore import a cross-origin, CORS-tainted video as a WebGPU external texture, sample it in a shader, and read the result back through a GPU buffer — exfiltrating pixels of a cross-origin video that the Same-Origin Policy is meant to keep unreadable (the same protection canvas enforces by throwing on getImageData for tainted content).

The fix threads the ScriptExecutionContext into importExternalTexture ([CallWith=CurrentScriptExecutionContext] in the IDL, new parameter in the .h/.cpp) and, before importing, checks (*videoElement)->taintsOrigin(context.securityOrigin()); if the video would taint the caller’s origin it returns a SecurityError (‘Cross origin external videos are not allowed in WebGPU’) instead of importing.

The restored invariant is that only same-origin (or CORS-approved) video pixels can enter the WebGPU pipeline, matching canvas taint rules. The layout test confirms importExternalTexture throws SecurityError for a cross-origin tainted video. Established by the diff.

Key insight
WebGPU’s importExternalTexture was a new pixel-ingestion path that never learned the taint rule canvas already enforces. The fix threads the caller’s ScriptExecutionContext (and thus security origin) into the import and rejects tainted video with a SecurityError — closing an SOP hole opened by a newer API surface.

Attack Path

  1. Load a cross-origin video without CORS The page creates a <video> whose src is a cross-origin URL and no crossorigin attribute, so it becomes origin-tainted.
  2. Import it as a WebGPU external texture Pre-patch, device.importExternalTexture({source: video}) accepts the tainted video with no origin check.
  3. Sample and read back the pixels A compute/render shader samples the external texture and writes results to a GPU buffer the page can map and read.
  4. Exfiltrate cross-origin video content The page reads back pixel-derived data from the cross-origin video, defeating the Same-Origin Policy (now blocked with a SecurityError).

Impact Assessment

A cross-origin information disclosure that defeats the Same-Origin Policy for video content. An attacker page can read the pixels of any cross-origin video the victim can load (e.g. authenticated video behind a session), exfiltrating frames that SOP is designed to keep unreadable. No memory corruption is needed — the leak is purely a policy bypass, which makes it reliable and stealthy.

Changed Functions

FunctionChangeNotes
if
LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video.html
modified

Files Changed

  • LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video-expected.txt
  • LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video.html
  • Source/WebCore/Modules/WebGPU/GPUDevice.cpp
  • Source/WebCore/Modules/WebGPU/GPUDevice.h
  • Source/WebCore/Modules/WebGPU/GPUDevice.idl

Audit Directions

  • New media-ingestion APIs vs. taint
    Audit every WebGPU/WebGL entry point that accepts an HTMLVideoElement, HTMLImageElement, or canvas for a taintsOrigin() check equivalent to canvas’s; new GPU surfaces are the likeliest to miss it.
  • Origin plumbing
    Verify IDL methods that need the caller origin actually receive the execution context (e.g. [CallWith=CurrentScriptExecutionContext]) rather than inferring same-origin.
diff --git a/Source/WebCore/Modules/WebGPU/GPUDevice.cpp b/Source/WebCore/Modules/WebGPU/GPUDevice.cpp
index ffa125690be7..9c552366b081 100644
--- a/Source/WebCore/Modules/WebGPU/GPUDevice.cpp
+++ b/Source/WebCore/Modules/WebGPU/GPUDevice.cpp
@@ -389,8 +389,10 @@ private:
 };
 #endif
 
-ExceptionOr<Ref<GPUExternalTexture>> GPUDevice::importExternalTexture(const GPUExternalTextureDescriptor& externalTextureDescriptor)
+ExceptionOr<Ref<GPUExternalTexture>> GPUDevice::importExternalTexture(ScriptExecutionContext& context, const GPUExternalTextureDescriptor& externalTextureDescriptor)
 {
+    UNUSED_PARAM(context);
+
 #if ENABLE(VIDEO) && PLATFORM(COCOA)
     if (RefPtr externalTexture = externalTextureForDescriptor(externalTextureDescriptor)) {
         externalTexture->undestroy();
@@ -416,6 +418,9 @@ ExceptionOr<Ref<GPUExternalTexture>> GPUDevice::importExternalTexture(const GPUE
 #else
     if (auto* videoElement = &externalTextureDescriptor.source; videoElement && videoElement->get()) {
 #endif
+        if ((*videoElement)->taintsOrigin(*protect(context.securityOrigin()).get()))
+            return Exception { ExceptionCode::SecurityError, "GPUDevice.importExternalTexture: Cross origin external videos are not allowed in WebGPU"_s };
+
         WeakPtr<HTMLVideoElement> videoElementPtr = videoElement->get();
         m_videoElementToExternalTextureMap.set(*videoElementPtr, externalTexture.get());
         m_previouslyImportedExternalTexture.first = *videoElement;
diff --git a/Source/WebCore/Modules/WebGPU/GPUDevice.h b/Source/WebCore/Modules/WebGPU/GPUDevice.h
index 06d0f34d1938..5a23d2780f89 100644
--- a/Source/WebCore/Modules/WebGPU/GPUDevice.h
+++ b/Source/WebCore/Modules/WebGPU/GPUDevice.h
@@ -118,7 +118,7 @@ public:
     ExceptionOr<Ref<GPUTexture>> createTexture(const GPUTextureDescriptor&);
     std::optional<String> errorValidatingSupportedFormat(GPUTextureFormat) const;
     ExceptionOr<Ref<GPUSampler>> createSampler(const std::optional<GPUSamplerDescriptor>&);
-    ExceptionOr<Ref<GPUExternalTexture>> importExternalTexture(const GPUExternalTextureDescriptor&);
+    ExceptionOr<Ref<GPUExternalTexture>> importExternalTexture(ScriptExecutionContext&, const GPUExternalTextureDescriptor&);
 
     ExceptionOr<Ref<GPUBindGroupLayout>> createBindGroupLayout(const GPUBindGroupLayoutDescriptor&);
     ExceptionOr<Ref<GPUPipelineLayout>> createPipelineLayout(const GPUPipelineLayoutDescriptor&);
diff --git a/Source/WebCore/Modules/WebGPU/GPUDevice.idl b/Source/WebCore/Modules/WebGPU/GPUDevice.idl
index 9eaeb306fd1f..dfc988d54b4c 100644
--- a/Source/WebCore/Modules/WebGPU/GPUDevice.idl
+++ b/Source/WebCore/Modules/WebGPU/GPUDevice.idl
@@ -42,7 +42,7 @@
     GPUBuffer createBuffer(GPUBufferDescriptor descriptor);
     GPUTexture createTexture(GPUTextureDescriptor descriptor);
     GPUSampler createSampler(optional GPUSamplerDescriptor descriptor);
-    GPUExternalTexture importExternalTexture(GPUExternalTextureDescriptor descriptor);
+    [CallWith=CurrentScriptExecutionContext] GPUExternalTexture importExternalTexture(GPUExternalTextureDescriptor descriptor);
 
     GPUBindGroupLayout createBindGroupLayout(GPUBindGroupLayoutDescriptor descriptor);
     GPUPipelineLayout createPipelineLayout(GPUPipelineLayoutDescriptor descriptor);
diff --git a/LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video-expected.txt b/LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video-expected.txt
new file mode 100644
index 000000000000..71010fe2c119
--- /dev/null
+++ b/LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video-expected.txt
@@ -0,0 +1,2 @@
+CONSOLE MESSAGE: Unable to get image data from canvas because the canvas has been tainted by cross-origin data.
+PASS: importExternalTexture threw SecurityError for a cross-origin tainted video
diff --git a/LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video.html b/LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video.html
new file mode 100644
index 000000000000..dc1a12054276
--- /dev/null
+++ b/LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>importExternalTexture should throw SecurityError for cross-origin tainted video</title>
+</head>
+<body>
+<script>
+if (window.testRunner) {
+    testRunner.waitUntilDone();
+    testRunner.dumpAsText();
+}
+
+function log(msg) {
+    document.body.appendChild(document.createTextNode(msg + '\n'));
+}
+
+async function runTest() {
+    if (!navigator.gpu) {
+        log('SKIP: WebGPU not available');
+        if (window.testRunner)
+            testRunner.notifyDone();
+        return;
+    }
+
+    const adapter = await navigator.gpu.requestAdapter();
+    if (!adapter) {
+        log('SKIP: No WebGPU adapter');
+        if (window.testRunner)
+            testRunner.notifyDone();
+        return;
+    }
+    const device = await adapter.requestDevice();
+
+    // Load a video from a cross-origin URL (localhost vs 127.0.0.1).
+    // No crossorigin attribute means the video becomes origin-tainted.
+    const video = document.createElement('video');
+    video.muted = true;
+    video.playsInline = true;
+    // This test is served from 127.0.0.1:8000, so localhost:8000 is cross-origin.
+    video.src = 'http://localhost:8000/webgpu/resources/red-green.mp4';
+
+    await new Promise((resolve, reject) => {
+        const timeout = setTimeout(() => reject(new Error('Video load timed out')), 10000);
+        video.addEventListener('canplay', () => { clearTimeout(timeout); resolve(); }, { once: true });
+        video.addEventListener('error', () => { clearTimeout(timeout); reject(new Error('Video load error')); }, { once: true });
+        video.play().catch(() => {});
+    });
+
+    // Confirm the video is genuinely cross-origin tainted: canvas getImageData must throw.
+    const taintCheck = document.createElement('canvas');
+    taintCheck.width = 1;
+    taintCheck.height = 1;
+    const ctx = taintCheck.getContext('2d');
+    ctx.drawImage(video, 0, 0, 1, 1);
+    try {
+        ctx.getImageData(0, 0, 1, 1);
+        log('FAIL: Canvas did not block cross-origin pixel readback — video lacks CORS taint, test is invalid');
+        if (window.testRunner)
+            testRunner.notifyDone();
+        return;
+    } catch (e) {
+        if (e.name !== 'SecurityError') {
+            log('FAIL: Canvas threw unexpected error: ' + e.name + ': ' + e.message);
+            if (window.testRunner)
+                testRunner.notifyDone();
+            return;
+        }
+        // SecurityError thrown — video is correctly tainted, proceed.
+    }
+
+    // importExternalTexture with a cross-origin tainted video must throw SecurityError.
+    try {
+        device.importExternalTexture({ source: video });
+        log('FAIL: importExternalTexture did not throw for a cross-origin tainted video');
+    } catch (e) {
+        if (e.name === 'SecurityError')
+            log('PASS: importExternalTexture threw SecurityError for a cross-origin tainted video');
+        else
+            log('FAIL: importExternalTexture threw unexpected error: ' + e.name + ': ' + e.message);
+    }
+
+    if (window.testRunner)
+        testRunner.notifyDone();
+}
+
+runTest().catch(e => {
+    document.body.appendChild(document.createTextNode('FAIL: ' + e.name + ': ' + e.message + '\n'));
+    if (window.testRunner)
+        testRunner.notifyDone();
+});
+</script>
+</body>
+</html>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video-expected.txt b/LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video-expected.txt
new file mode 100644
index 000000000000..71010fe2c119
--- /dev/null
+++ b/LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video-expected.txt
@@ -0,0 +1,2 @@
+CONSOLE MESSAGE: Unable to get image data from canvas because the canvas has been tainted by cross-origin data.
+PASS: importExternalTexture threw SecurityError for a cross-origin tainted video
diff --git a/LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video.html b/LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video.html
new file mode 100644
index 000000000000..dc1a12054276
--- /dev/null
+++ b/LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>importExternalTexture should throw SecurityError for cross-origin tainted video</title>
+</head>
+<body>
+<script>
+if (window.testRunner) {
+    testRunner.waitUntilDone();
+    testRunner.dumpAsText();
+}
+
+function log(msg) {
+    document.body.appendChild(document.createTextNode(msg + '\n'));
+}
+
+async function runTest() {
+    if (!navigator.gpu) {
+        log('SKIP: WebGPU not available');
+        if (window.testRunner)
+            testRunner.notifyDone();
+        return;
+    }
+
+    const adapter = await navigator.gpu.requestAdapter();
+    if (!adapter) {
+        log('SKIP: No WebGPU adapter');
+        if (window.testRunner)
+            testRunner.notifyDone();
+        return;
+    }
+    const device = await adapter.requestDevice();
+
+    // Load a video from a cross-origin URL (localhost vs 127.0.0.1).
+    // No crossorigin attribute means the video becomes origin-tainted.
+    const video = document.createElement('video');
+    video.muted = true;
+    video.playsInline = true;
+    // This test is served from 127.0.0.1:8000, so localhost:8000 is cross-origin.
+    video.src = 'http://localhost:8000/webgpu/resources/red-green.mp4';
+
+    await new Promise((resolve, reject) => {
+        const timeout = setTimeout(() => reject(new Error('Video load timed out')), 10000);
+        video.addEventListener('canplay', () => { clearTimeout(timeout); resolve(); }, { once: true });
+        video.addEventListener('error', () => { clearTimeout(timeout); reject(new Error('Video load error')); }, { once: true });
+        video.play().catch(() => {});
+    });
+
+    // Confirm the video is genuinely cross-origin tainted: canvas getImageData must throw.
+    const taintCheck = document.createElement('canvas');
+    taintCheck.width = 1;
+    taintCheck.height = 1;
+    const ctx = taintCheck.getContext('2d');
+    ctx.drawImage(video, 0, 0, 1, 1);
+    try {
+        ctx.getImageData(0, 0, 1, 1);
+        log('FAIL: Canvas did not block cross-origin pixel readback — video lacks CORS taint, test is invalid');
+        if (window.testRunner)
+            testRunner.notifyDone();
+        return;
+    } catch (e) {
+        if (e.name !== 'SecurityError') {
+            log('FAIL: Canvas threw unexpected error: ' + e.name + ': ' + e.message);
+            if (window.testRunner)
+                testRunner.notifyDone();
+            return;
+        }
+        // SecurityError thrown — video is correctly tainted, proceed.
+    }
+
+    // importExternalTexture with a cross-origin tainted video must throw SecurityError.
+    try {
+        device.importExternalTexture({ source: video });
+        log('FAIL: importExternalTexture did not throw for a cross-origin tainted video');
+    } catch (e) {
+        if (e.name === 'SecurityError')
+            log('PASS: importExternalTexture threw SecurityError for a cross-origin tainted video');
+        else
+            log('FAIL: importExternalTexture threw unexpected error: ' + e.name + ': ' + e.message);
+    }
+
+    if (window.testRunner)
+        testRunner.notifyDone();
+}
+
+runTest().catch(e => {
+    document.body.appendChild(document.createTextNode('FAIL: ' + e.name + ': ' + e.message + '\n'));
+    if (window.testRunner)
+        testRunner.notifyDone();
+});
+</script>
+</body>
+</html>
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.