Released June 29, 2026

CVE-2026-43735

CVSS 8.1 severity high CWE-352 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N

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.cpp
  • Source/WebCore/Modules/WebGPU/GPUDevice.h
  • Source/WebCore/Modules/WebGPU/GPUDevice.idl

Test Files

  • LayoutTests/http/tests/webgpu/import-external-texture-cross-origin-video-expected.txt
  • LayoutTests/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>