CVE-2026-43735
Overview
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.
Attack Path
- 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.
- Import it as a WebGPU external texture Pre-patch, device.importExternalTexture({source: video}) accepts the tainted video with no origin check.
- 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.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifLayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video.html |
modified |
Files Changed
LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video-expected.txtLayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video.htmlSource/WebCore/Modules/WebGPU/GPUDevice.cppSource/WebCore/Modules/WebGPU/GPUDevice.hSource/WebCore/Modules/WebGPU/GPUDevice.idl
Audit Directions
- New media-ingestion APIs vs. taintAudit 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 plumbingVerify IDL methods that need the caller origin actually receive the execution context (e.g. [CallWith=CurrentScriptExecutionContext]) rather than inferring same-origin.
Patch
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>
Regression Test / PoC
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>