From 39445d6dd689802b60383384bf06b79d497a1be9 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Tue, 28 Jan 2025 10:47:32 +0000 Subject: [PATCH] LibWeb: Implement basic high resolution time coarsening Several interfaces that return a high resolution time require that time to be coarsened, in order to prevent timing attacks. This implementation simply reduces the resolution of the returned timestamp to the minimum values given in the specification. Further work may be needed to make our implementation more robust to the kind of attacks that this mechanism is designed to prevent. --- Libraries/LibWeb/DOM/Document.cpp | 3 +- .../LibWeb/HighResolutionTime/TimeOrigin.cpp | 16 ++++++- .../Event-timestamp-safe-resolution.txt | 4 +- .../wpt-import/hr-time/timing-attack.txt | 6 +++ .../PerformanceObserver_basic.html | 12 ++++++ .../hr-time/resources/timing-attack.js | 42 +++++++++++++++++++ .../wpt-import/hr-time/timing-attack.html | 22 ++++++++++ 7 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/hr-time/timing-attack.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/hr-time/resources/timing-attack.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/hr-time/timing-attack.html diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index edbd84152a0..42267dcb6cc 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -330,7 +330,8 @@ WebIDL::ExceptionOr> Document::create_and_initialize(Type type DOM::DocumentLoadTimingInfo load_timing_info; // AD-HOC: The response object no longer has an associated timing info object. For now, we use response's non-standard response time property, // which represents the time that the time that the response object was created. - load_timing_info.navigation_start_time = navigation_params.response->response_time().nanoseconds() / 1e6; + auto response_creation_time = navigation_params.response->response_time().nanoseconds() / 1e6; + load_timing_info.navigation_start_time = HighResolutionTime::coarsen_time(response_creation_time, HTML::relevant_settings_object(*window).cross_origin_isolated_capability() == HTML::CanUseCrossOriginIsolatedAPIs::Yes); // 9. Let document be a new Document, with // type: type diff --git a/Libraries/LibWeb/HighResolutionTime/TimeOrigin.cpp b/Libraries/LibWeb/HighResolutionTime/TimeOrigin.cpp index c5ef9e9ca4a..bf0e7291918 100644 --- a/Libraries/LibWeb/HighResolutionTime/TimeOrigin.cpp +++ b/Libraries/LibWeb/HighResolutionTime/TimeOrigin.cpp @@ -5,6 +5,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -46,8 +47,19 @@ DOMHighResTimeStamp get_time_origin_timestamp(JS::Object const& global) // https://w3c.github.io/hr-time/#dfn-coarsen-time DOMHighResTimeStamp coarsen_time(DOMHighResTimeStamp timestamp, bool cross_origin_isolated_capability) { - // FIXME: Implement this. - (void)cross_origin_isolated_capability; + // 1. Let time resolution be 100 microseconds, or a higher implementation-defined value. + auto time_resolution_milliseconds = 0.1; + + // 2. If crossOriginIsolatedCapability is true, set time resolution to be 5 microseconds, or a higher implementation-defined value. + if (cross_origin_isolated_capability) + time_resolution_milliseconds = 0.005; + + // 3. In an implementation-defined manner, coarsen and potentially jitter timestamp such that its resolution will not exceed time resolution + timestamp = floor(timestamp / time_resolution_milliseconds) * time_resolution_milliseconds; + + // FIXME: Applying jitter to the coarsened timestamp here may decrease our susceptibility to timing attacks. + + // 4. Return timestamp as a moment return timestamp; } diff --git a/Tests/LibWeb/Text/expected/wpt-import/dom/events/Event-timestamp-safe-resolution.txt b/Tests/LibWeb/Text/expected/wpt-import/dom/events/Event-timestamp-safe-resolution.txt index e3132290428..acd74bf53c2 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/dom/events/Event-timestamp-safe-resolution.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/dom/events/Event-timestamp-safe-resolution.txt @@ -2,5 +2,5 @@ Harness status: OK Found 1 tests -1 Fail -Fail Event timestamp should not have a resolution better than 5 microseconds \ No newline at end of file +1 Pass +Pass Event timestamp should not have a resolution better than 5 microseconds \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/hr-time/timing-attack.txt b/Tests/LibWeb/Text/expected/wpt-import/hr-time/timing-attack.txt new file mode 100644 index 00000000000..c10c9a21cce --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/hr-time/timing-attack.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass The recommended minimum resolution of the Performance interface has been set to 100 microseconds for cross-origin isolated contexts. \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/PerformanceObserver/PerformanceObserver_basic.html b/Tests/LibWeb/Text/input/PerformanceObserver/PerformanceObserver_basic.html index a35b890342c..db9001a9cfc 100644 --- a/Tests/LibWeb/Text/input/PerformanceObserver/PerformanceObserver_basic.html +++ b/Tests/LibWeb/Text/input/PerformanceObserver/PerformanceObserver_basic.html @@ -5,6 +5,14 @@ bufferedMessages.push(message); } + function synchronousWaitMicroseconds(microseconds) { + var start = performance.now() * 1000, + now = start; + while (now - start < microseconds) { + now = performance.now() * 1000; + } + } + const globalObserver = new PerformanceObserver((list, observer) => { printlnBuffered(`observer === globalObserver: ${observer === globalObserver}`); printlnBuffered( @@ -53,7 +61,11 @@ globalObserver.observe({ entryTypes: ["measure", "mark"] }); const startMark = performance.mark("start"); + // The resolution of the clock used by the Performance interface is 100 microseconds, so we wait twice that time + // between calls to ensure they are ordered as we expect. + synchronousWaitMicroseconds(200); const endMark = performance.mark("end"); + synchronousWaitMicroseconds(200); const measureMark = performance.measure("measure", "start", "end"); function printCatchedException(func) { diff --git a/Tests/LibWeb/Text/input/wpt-import/hr-time/resources/timing-attack.js b/Tests/LibWeb/Text/input/wpt-import/hr-time/resources/timing-attack.js new file mode 100644 index 00000000000..f1fc786903a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/hr-time/resources/timing-attack.js @@ -0,0 +1,42 @@ +function run_test(isolated) { + let resolution = 100; + if (isolated) { + resolution = 5; + } + test(function() { + function check_resolutions(times, length) { + const end = length - 2; + + // we compare each value with the following ones + for (let i = 0; i < end; i++) { + const h1 = times[i]; + for (let j = i+1; j < end; j++) { + const h2 = times[j]; + const diff = h2 - h1; + assert_true((diff === 0) || ((diff * 1000) >= resolution), + "Differences smaller than ' + resolution + ' microseconds: " + diff); + } + } + return true; + } + + const times = new Array(10); + let index = 0; + let hrt1, hrt2, hrt; + assert_equals(self.crossOriginIsolated, isolated, "Document cross-origin isolated value matches"); + + // rapid firing of performance.now + hrt1 = performance.now(); + hrt2 = performance.now(); + times[index++] = hrt1; + times[index++] = hrt2; + + // ensure that we get performance.now() to return a different value + do { + hrt = performance.now(); + times[index++] = hrt; + } while ((hrt - hrt1) === 0); + + assert_true(check_resolutions(times, index), 'Difference should be at least ' + resolution + ' microseconds.'); + }, 'The recommended minimum resolution of the Performance interface has been set to ' + resolution + ' microseconds for cross-origin isolated contexts.'); +} diff --git a/Tests/LibWeb/Text/input/wpt-import/hr-time/timing-attack.html b/Tests/LibWeb/Text/input/wpt-import/hr-time/timing-attack.html new file mode 100644 index 00000000000..1db71f65586 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/hr-time/timing-attack.html @@ -0,0 +1,22 @@ + + + + +window.performance.now should not enable timing attacks + + + + + + + + +

Description

+

The recommended minimum resolution of the Performance interface should be set to 100 microseconds for non-isolated contexts.

+ +
+ + +