ladybird/Libraries/LibWeb/WebDriver/Screenshot.cpp
sideshowbarker 9f7a328d9b 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
2026-06-18 10:52:42 +02:00

106 lines
5.3 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGfx/Bitmap.h>
#include <LibGfx/PaintingSurface.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/ElementFactory.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/CanvasRenderingContext2D.h>
#include <LibWeb/HTML/HTMLCanvasElement.h>
#include <LibWeb/HTML/ImageBitmap.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/TagNames.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/WebDriver/Screenshot.h>
namespace Web::WebDriver {
// https://w3c.github.io/webdriver/#dfn-draw-a-bounding-box-from-the-framebuffer
ErrorOr<GC::Ref<HTML::HTMLCanvasElement>, WebDriver::Error> draw_bounding_box_from_the_framebuffer(HTML::BrowsingContext& browsing_context, DOM::Element& element, Gfx::IntRect rect)
{
HTML::TemporaryExecutionContext execution_context { element.realm() };
// 1. If either the initial viewport's width or height is 0 CSS pixels, return error with error code unable to capture screen.
auto viewport_rect = browsing_context.top_level_traversable()->viewport_rect();
if (viewport_rect.is_empty())
return Error::from_code(ErrorCode::UnableToCaptureScreen, "Viewport is empty"sv);
auto viewport_device_rect = browsing_context.page().enclosing_device_rect(viewport_rect).to_type<int>();
// 2. Let paint width be the initial viewport's width min(rectangle x coordinate, rectangle x coordinate + rectangle width dimension).
auto paint_width = viewport_device_rect.width() - min(rect.x(), rect.x() + rect.width());
// 3. Let paint height be the initial viewport's height min(rectangle y coordinate, rectangle y coordinate + rectangle height dimension).
auto paint_height = viewport_device_rect.height() - min(rect.y(), rect.y() + rect.height());
// 4. Let canvas be a new canvas element, and set its width and height to paint width and paint height, respectively.
auto canvas_element = DOM::create_element(element.document(), HTML::TagNames::canvas, Namespace::HTML).release_value_but_fixme_should_propagate_errors();
auto& canvas = as<HTML::HTMLCanvasElement>(*canvas_element);
// FIXME: Handle DevicePixelRatio in HiDPI mode.
canvas.set_width(paint_width);
canvas.set_height(paint_height);
// FIXME: 5. Let context, a canvas context mode, be the result of invoking the 2D context creation algorithm given canvas as the target.
MUST(canvas.create_2d_context({}));
// 6. Complete implementation specific steps equivalent to drawing the region of the framebuffer specified by the following coordinates onto context:
// - X coordinate: rectangle x coordinate
// - Y coordinate: rectangle y coordinate
// - Width: paint width
// - Height: paint height
Gfx::IntRect paint_rect { rect.x(), rect.y(), paint_width, paint_height };
auto bitmap = MUST(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, Gfx::AlphaType::Premultiplied, Gfx::IntSize { paint_width, paint_height }));
auto painting_surface = Gfx::PaintingSurface::wrap_bitmap(bitmap);
IGNORE_USE_IN_ESCAPING_LAMBDA bool did_paint = false;
HTML::PaintConfig paint_config { .canvas_fill_rect = paint_rect };
browsing_context.active_document()->navigable()->render_screenshot(painting_surface, paint_config, [&did_paint] {
did_paint = true;
});
HTML::main_thread_event_loop().spin_until(GC::create_function(HTML::main_thread_event_loop().heap(), [&] {
return did_paint;
}));
auto image_bitmap = HTML::ImageBitmap::create(element.realm());
image_bitmap->set_bitmap(bitmap);
if (canvas.canvas_rendering_context_2d()->draw_image(image_bitmap, 0, 0).is_exception())
return Error::from_code(ErrorCode::UnableToCaptureScreen, "Failed to draw the screenshot to the canvas"sv);
// 7. Return success with canvas.
return canvas;
}
// https://w3c.github.io/webdriver/#dfn-encoding-a-canvas-as-base64
Response encode_canvas_element(HTML::HTMLCanvasElement& canvas)
{
// 1. If the canvas elements bitmaps 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 elements 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())
return Error::from_code(ErrorCode::UnableToCaptureScreen, "Captured screenshot is empty"sv);
// 3. Let file be a serialization of the canvas elements bitmap as a file, using "image/png" as an argument.
// 4. Let data url be a data: URL representing file. [RFC2397]
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(',');
VERIFY(index.has_value());
// 6. Let encoded string be a substring of data url using (index + 1) as the start argument.
auto encoded_string = MUST(data_url.substring_from_byte_offset(*index + 1));
// 7. Return success with data encoded string.
return JsonValue { move(encoded_string) };
}
}