CVE-2026-43735
Overview
Impact:
Description: A malicious website may exfiltrate data cross-origin
Researchers: Rhyru9, Merrick Hare, Kwak Kiyong, Song Nuri, Khai Tran, John Lussier, Gurpreet Shergill, Drinor Selmanaj (Sentry)
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.
Patch Details
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>
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);
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2026-43735 |
| Bugzilla | 313357 |
| Component | WebCore |
| Bug Class | CrossOrigin |
| Severity | medium |
| Commit | 8254b44eba7e8bfb… |
| Advisory | Apple Advisory |
Terminology
- 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
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.
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.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
GPUDevice::importExternalTexture |
Source/WebCore/Modules/WebGPU/GPUDevice.cpp |
modified | Takes a ScriptExecutionContext and rejects the import with a SecurityError when the source video taintsOrigin(context.securityOrigin()), preventing cross-origin tainted video from entering WebGPU. |
GPUDevice::importExternalTexture (declaration) |
Source/WebCore/Modules/WebGPU/GPUDevice.h |
modified | Signature adds the ScriptExecutionContext& parameter. |
GPUDevice.importExternalTexture (IDL) |
Source/WebCore/Modules/WebGPU/GPUDevice.idl |
modified | Adds [CallWith=CurrentScriptExecutionContext] so the caller’s execution context (and thus security origin) is available for the taint check. |
Files Changed
Source Files
Source/WebCore/Modules/WebGPU/GPUDevice.cppSource/WebCore/Modules/WebGPU/GPUDevice.hSource/WebCore/Modules/WebGPU/GPUDevice.idl
Test Files
LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video-expected.txtLayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video.html
Regression Test
The fix ships this proof-of-concept / regression test:
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>