LibWeb: Reject canvas toDataURL()/toBlob() when not origin-clean
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
This commit is contained in:
parent
d642f7d85a
commit
9f7a328d9b
7 changed files with 105 additions and 7 deletions
|
|
@ -61,6 +61,9 @@ public:
|
|||
static constexpr bool OVERRIDES_FINALIZE = true;
|
||||
|
||||
static JS::ThrowCompletionOr<GC::Ref<CanvasRenderingContext2D>> create(JS::Realm&, HTMLCanvasElement&, JS::Value options);
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-origin-clean
|
||||
bool origin_clean() const { return m_origin_clean; }
|
||||
virtual ~CanvasRenderingContext2D() override;
|
||||
|
||||
virtual void fill_rect(float x, float y, float width, float height) override;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
#include <LibWeb/WebGL/WebGLContextProxy.h>
|
||||
#include <LibWeb/WebGL/WebGLRenderingContext.h>
|
||||
#include <LibWeb/WebIDL/AbstractOperations.h>
|
||||
#include <LibWeb/WebIDL/DOMException.h>
|
||||
|
||||
namespace Web::HTML {
|
||||
|
||||
|
|
@ -333,10 +334,21 @@ Gfx::IntSize HTMLCanvasElement::bitmap_size_for_canvas(size_t minimum_width, siz
|
|||
return Gfx::IntSize(width, height);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-todataurl
|
||||
String HTMLCanvasElement::to_data_url(StringView type, Optional<JS::Value> js_quality)
|
||||
// https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-origin-clean
|
||||
bool HTMLCanvasElement::is_origin_clean() const
|
||||
{
|
||||
// FIXME: 1. If this canvas element's bitmap's origin-clean flag is set to false, then throw a "SecurityError" DOMException.
|
||||
return m_context.visit(
|
||||
[](GC::Ref<CanvasRenderingContext2D> const& context) { return context->origin_clean(); },
|
||||
// FIXME: WebGL and WebGL2 contexts do not track the origin-clean flag yet.
|
||||
[](auto const&) { return true; });
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-todataurl
|
||||
WebIDL::ExceptionOr<String> HTMLCanvasElement::to_data_url(StringView type, Optional<JS::Value> js_quality)
|
||||
{
|
||||
// 1. If this canvas element's bitmap's origin-clean flag is set to false, then throw a "SecurityError" DOMException.
|
||||
if (!is_origin_clean())
|
||||
return WebIDL::SecurityError::create(realm(), "Canvas is not origin-clean"_utf16);
|
||||
|
||||
// 2. If this canvas element's bitmap has no pixels (i.e. either its horizontal dimension or its vertical dimension is zero),
|
||||
// then return the string "data:,". (This is the shortest data: URL; it represents the empty string in a text/plain resource.)
|
||||
|
|
@ -365,7 +377,9 @@ String HTMLCanvasElement::to_data_url(StringView type, Optional<JS::Value> js_qu
|
|||
// https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-toblob
|
||||
WebIDL::ExceptionOr<void> HTMLCanvasElement::to_blob(GC::Ref<WebIDL::CallbackType> callback, StringView type, Optional<JS::Value> js_quality)
|
||||
{
|
||||
// FIXME: 1. If this canvas element's bitmap's origin-clean flag is set to false, then throw a "SecurityError" DOMException.
|
||||
// 1. If this canvas element's bitmap's origin-clean flag is set to false, then throw a "SecurityError" DOMException.
|
||||
if (!is_origin_clean())
|
||||
return WebIDL::SecurityError::create(realm(), "Canvas is not origin-clean"_utf16);
|
||||
|
||||
// 2. Let result be null.
|
||||
// 3. If this canvas element's bitmap has pixels (i.e., neither its horizontal dimension nor its vertical dimension is zero),
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@ public:
|
|||
|
||||
virtual void attribute_changed(FlyString const& local_name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
|
||||
|
||||
String to_data_url(StringView type, Optional<JS::Value> quality);
|
||||
WebIDL::ExceptionOr<String> to_data_url(StringView type, Optional<JS::Value> quality);
|
||||
WebIDL::ExceptionOr<void> to_blob(GC::Ref<WebIDL::CallbackType> callback, StringView type, Optional<JS::Value> quality);
|
||||
bool is_origin_clean() const;
|
||||
RefPtr<Gfx::Bitmap> get_bitmap_from_surface();
|
||||
|
||||
void prepare_for_compositing();
|
||||
|
|
|
|||
|
|
@ -80,7 +80,9 @@ ErrorOr<GC::Ref<HTML::HTMLCanvasElement>, WebDriver::Error> draw_bounding_box_fr
|
|||
// https://w3c.github.io/webdriver/#dfn-encoding-a-canvas-as-base64
|
||||
Response encode_canvas_element(HTML::HTMLCanvasElement& canvas)
|
||||
{
|
||||
// FIXME: 1. If the canvas element’s bitmap’s origin-clean flag is set to false, return error with error code unable to capture screen.
|
||||
// 1. If the canvas element’s bitmap’s origin-clean flag is set to false, return error with error code unable to capture screen.
|
||||
if (!canvas.is_origin_clean())
|
||||
return Error::from_code(ErrorCode::UnableToCaptureScreen, "Canvas is not origin-clean"sv);
|
||||
|
||||
// 2. If the canvas element’s bitmap has no pixels (i.e. either its horizontal dimension or vertical dimension is zero) then return error with error code unable to capture screen.
|
||||
if (!canvas.canvas_surface_content_size().has_value())
|
||||
|
|
@ -88,7 +90,7 @@ Response encode_canvas_element(HTML::HTMLCanvasElement& canvas)
|
|||
|
||||
// 3. Let file be a serialization of the canvas element’s bitmap as a file, using "image/png" as an argument.
|
||||
// 4. Let data url be a data: URL representing file. [RFC2397]
|
||||
auto data_url = canvas.to_data_url("image/png"sv, JS::js_undefined());
|
||||
auto data_url = MUST(canvas.to_data_url("image/png"sv, JS::js_undefined()));
|
||||
|
||||
// 5. Let index be the index of "," in data url.
|
||||
auto index = data_url.find_byte_offset(',');
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ Text/input/HTML/parser-streams-bytes.html
|
|||
Text/input/HTML/parser-streams-with-document-write.html
|
||||
Text/input/HTML/parser-streams-utf8-split.html
|
||||
|
||||
; This test needs to taint a canvas with a cross-origin image.
|
||||
Text/input/HTML/canvas-toDataURL-toBlob-origin-clean.html
|
||||
|
||||
; Navigation has entries and events disabled for opaque origins, so this crash only reproduces over HTTP.
|
||||
Crash/DOM/document-open-navigation-api.html
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
untainted toDataURL ok: true
|
||||
tainted toDataURL: SecurityError
|
||||
tainted toBlob: SecurityError
|
||||
tainted getImageData: SecurityError
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<!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>
|
||||
Loading…
Reference in a new issue