Problem: Drawing a cross-origin image onto a 2D canvas clears its origin-clean flag, but toDataURL() and toBlob() ignored that flag and serialized the bitmap regardless. So, a page could read back the cross-origin pixels it shouldn't (per spec) be allowed to access. Cause: The origin-clean checks in to_data_url() and to_blob() were left as FIXMEs. Only getImageData() enforced the flag. Fix: Throw a SecurityError exception from both serialization entry points when the canvas isn't origin-clean — matching getImageData() and the spec. The same check also implements the previously-stubbed origin-clean step in the WebDriver canvas-encoding algorithm. Fixes: https://github.com/LadybirdBrowser/ladybird/issues/10009
71 lines
2.6 KiB
HTML
71 lines
2.6 KiB
HTML
<!DOCTYPE html>
|
|
<script src="../include.js"></script>
|
|
<script>
|
|
// Drawing a cross-origin (CORS-cross-origin) image onto a 2D canvas clears its origin-clean flag. toDataURL(),
|
|
// toBlob() and getImageData() must then all throw a "SecurityError" DOMException so cross-origin pixels can't be
|
|
// read back.
|
|
const imagePath = "/echo/canvas-toDataURL-toBlob-origin-clean.png";
|
|
|
|
function registerEchoResponse(response) {
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open("POST", "/echo", false);
|
|
xhr.setRequestHeader("Content-Type", "application/json");
|
|
xhr.send(JSON.stringify(response));
|
|
}
|
|
|
|
// 1x1 transparent PNG.
|
|
const transparentPng = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
|
|
registerEchoResponse({
|
|
method: "GET",
|
|
path: imagePath,
|
|
status: 200,
|
|
headers: {
|
|
"Cache-Control": "no-store",
|
|
"Content-Type": "image/png",
|
|
},
|
|
body_encoding: "base64",
|
|
body: transparentPng,
|
|
});
|
|
|
|
// A unique hostname on the echo server's port is a different origin, so the image (loaded without a crossorigin
|
|
// attribute) is CORS-cross-origin.
|
|
const crossOriginImageUrl = new URL(imagePath, location.href);
|
|
crossOriginImageUrl.hostname = uniqueLocalhostHostname("canvas-toDataURL-toBlob-origin-clean");
|
|
|
|
function report(label, fn) {
|
|
try {
|
|
fn();
|
|
println(`${label}: no throw`);
|
|
} catch (error) {
|
|
println(`${label}: ${error.name}`);
|
|
}
|
|
}
|
|
|
|
asyncTest(done => {
|
|
// An untainted canvas must still serialize normally (no over-blocking).
|
|
const cleanCanvas = document.createElement("canvas");
|
|
cleanCanvas.width = 1;
|
|
cleanCanvas.height = 1;
|
|
cleanCanvas.getContext("2d");
|
|
println(`untainted toDataURL ok: ${cleanCanvas.toDataURL().startsWith("data:image/png")}`);
|
|
|
|
const image = new Image();
|
|
image.onload = () => {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = 1;
|
|
canvas.height = 1;
|
|
const context = canvas.getContext("2d");
|
|
context.drawImage(image, 0, 0);
|
|
|
|
report("tainted toDataURL", () => canvas.toDataURL());
|
|
report("tainted toBlob", () => canvas.toBlob(() => {}));
|
|
report("tainted getImageData", () => context.getImageData(0, 0, 1, 1));
|
|
done();
|
|
};
|
|
image.onerror = () => {
|
|
println("tainted image load: FAILED");
|
|
done();
|
|
};
|
|
image.src = crossOriginImageUrl.href;
|
|
});
|
|
</script>
|