performance.now() намеренно огрублён, чтобы закрыть Spectre-атаки по времени. Как работает ограничение, почему изоляция возвращает точность и чего это стоит.
В августе 2026 года Cloudflare опубликовала материал, заново разбирающий атаки Spectre на Workers, с неприятным выводом: во время выполнения только на CPU Date.now() и performance.now() полностью заморожены — непрерывно идущих часов высокого разрешения нет вообще, — но обычного WebSocket-соединения с внешним сервером, отдающим временные метки, оказалось достаточно, чтобы стабильно вернуть субмиллисекундную точность. Локальная защита, обойдённая одолженными по сети часами. Браузеры упёрлись в точно такую же стену на годы раньше, по той же самой причине, и тоже не решили её до конца. Вот как именно был размыт performance.now(), что означает «размыт» в конкретных цифрах, и почему часы, которые читает ваш JavaScript, сами по себе — сигнал, по которому вас можно фингерпринтить.
Ключевые выводы
performance.now()намеренно огрублён, а не работает с багом — это защита от Spectre-класса атак по времени через кэш, которым для успеха нужны точные часы.- Спецификация W3C High Resolution Time определяет двухуровневую модель точности: около 100 микросекунд на обычной странице, около 5 микросекунд, когда страница кросс-доменно изолирована.
- Кросс-доменная изоляция — тот же порог COOP/COEP, который открывает
SharedArrayBuffer: эти две защиты идут в паре, потому что общий буфер, опрашиваемый в тесном цикле, сам по себе служит альтернативными часами. - Поведение самих часов теперь тоже сигнал для фингерпринтинга: какой уровень точности вы получаете, сколько джиттера добавлено сверху и намеренно ли какой-то режим защиты от фингерпринтинга огрубляет время ещё сильнее — всё это отличается от браузера к браузеру и от настройки к настройке.
- То же самое ограничение, что блокирует атаки по кэшу, ставит реальный предел точности и для легитимных измерений внутри браузера — находка Cloudflare про Workers и огрублённый
performance.now()в браузере — один и тот же компромисс, только на разных уровнях стека.
Зачем браузеры вообще ограничивают время
performance.now() не всегда был размытым. Изначальная цель дизайна, согласно справке MDN, — субмиллисекундная точность: монотонные часы, не зависящие от корректировок системного времени, достаточно точные, чтобы измерять время отдельных вызовов функций. Несколько лет именно это и работало.
Потом появился Spectre. Атаки на основе спекулятивного выполнения измеряют время доступа к памяти: попадание в кэш занимает наносекунды, промах — на несколько десятков наносекунд больше, и этого крошечного разрыва достаточно, чтобы вытянуть данные, которые атакующий не должен был видеть, — по биту за раз, через тысячи измерений. Атаке не нужно читать память напрямую. Ей нужны часы, достаточно точные, чтобы отличить попадание от промаха. Отнимите эти часы или сделайте их достаточно шумными — и тот же самый код, что чисто утекал данные, теперь утекает только шум.
Именно это обоснование прямо прописано в спецификации hr-time-3. Раздел о безопасности прямо называет угрозой «атаки по кэшу, статистический фингерпринтинг и микроархитектурные атаки»: вредоносный сайт может измерять время обычных браузерных операций, чтобы вычленить конкретного пользователя из массы или добраться до данных того же процесса, которых ему никто не отдавал. Ответ спецификации — определённый алгоритм «огрубления времени»: любой браузер, реализующий спецификацию, обязан снижать точность необработанной высокоточной временной метки, а сверху может добавлять джиттер и ограничивать частоту повторных вызовов. Производители браузеров выкатили это в течение недель после раскрытия Spectre в 2018 году, за одну ночь огрубив performance.now() с субмикросекундного шага до десятков и сотен микросекунд — а в некоторых движках и до целой миллисекунды, — и с тех пор он так и остаётся огрублённым.
Двухуровневая модель: что покупает кросс-доменная изоляция
Навсегда зажать все страницы в одну и ту же грубую точность значило бы сломать реальные сценарии измерений — обработка звука через WebAssembly, видеокодеки и научные вычисления в браузере опираются на performance.now(), чтобы получать по-настоящему полезное время. Компромисс спецификации — второй, более тонкий уровень, доступный только страницам, которые сами соглашаются на изоляцию.
Документ становится кросс-доменно изолированным, отдавая два заголовка ответа:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Эта пара разрывает связь страницы с кросс-доменными открывателями и всплывающими окнами и ограничивает, какие кросс-доменные ресурсы она может встраивать без их явного согласия. Взамен window.crossOriginIsolated становится true, и, согласно шагам самого алгоритма огрубления времени, ограничение смягчается: примерно 100 микросекунд точности на обычной странице, примерно 5 микросекунд при активной изоляции. Ни одно из этих чисел не жёсткая универсальная константа — спецификация формулирует оба как минимальную точность «или выше», так что отдельные движки вольны ограничивать ещё агрессивнее, и часто так и делают.
Тот же флаг изоляции открывает и SharedArrayBuffer. Это не случайное совпадение ради удобства: общий изменяемый буфер, который два потока опрашивают в тесном цикле, сам по себе — примитив для измерения времени, и исследователи Spectre как раз демонстрировали, как с его помощью восстановить высокоточные часы даже после того, как performance.now() был огрублён. Изоляция страницы закрывает обе двери одновременно — прямые часы и эти самодельные — поэтому они и управляются одной и той же парой заголовков, а не двумя отдельными переключателями.
Date.now() вообще стоит вне этой системы. Он был ограничен точностью в одну миллисекунду ещё до всего этого, отслеживает настенное время, а не монотонный счётчик, и не становится точнее под изоляцией — и алгоритм огрубления времени, и бонус от изоляции применяются именно к высокоточным таймерам, а не к исходным часам Date.
Со стороны фингерпринтинга: сами часы — это сигнал
Как только точность начинает зависеть от состояния изоляции, движка и настроек, часы перестают быть нейтральной инфраструктурой и становятся ещё одной вещью, которую страница может о вас узнать. Скрипт может измерить собственный джиттер performance.now() — вызывая его в тесном цикле и глядя на разрывы между последовательными отсчётами — и вывести, какой уровень точности он получает, что намекает на то, действительно ли страница изолирована так, как ожидается, и чей именно движок с его особой манерой округления породил этот паттерн.
Некоторые браузеры идут дальше намеренно. Настройка Firefox privacy.resistFingerprinting, всегда включённая в Tor Browser, поверх базовой Spectre-защиты по спецификации намеренно округляет таймеры до более грубых шагов — та же философия единообразия, которую мы подробно разбирали применительно к RFP в целом: пусть каждый защищённый браузер сообщает одни и те же грубые значения, чтобы джиттер таймера конкретного экземпляра не выделялся из толпы. Страница, которая строит фингерпринт на субмиллисекундном шуме таймера, получает гораздо меньше сигнала от браузера с включённым RFP, чем от обычного, — и это само по себе обнаруживаемое различие.
Это та же территория, что и в нашем общем руководстве по фингерпринтингу браузера: сигналу не обязательно быть уникальным идентификатором, чтобы быть полезным. Точность таймера и джиттер сами по себе несут низкую энтропию — они в основном говорят лишь «это Chromium» или «это Firefox с включённым RFP», а не выделяют одного конкретного посетителя из толпы, — но в сумме с десятками других сигналов это именно тот вклад, который количественно оценивает наша статья про энтропию фингерпринта и анонимные множества: маленький по отдельности, ненулевой в совокупности.
Если хочется не читать про этот набор сигналов, а увидеть его, проверка отпечатка от BrowserInsight работает целиком внутри вашего браузера и выкладывает рядом те атрибуты, которые страница может с вас считать, — canvas, WebGL, шрифты, подсказки о железе. Это честный способ оценить, сколько на самом деле добавляет к вашему профилю такой низкоэнтропийный сигнал, как поведение таймера.
Со стороны обнаружения: тайминг ловит то, чего эмуляция не подделает
Огрублённый или нет, performance.now() всё ещё достаточно точен, чтобы поймать кое-что другое: работу, которая на самом деле не выполнялась так, как заявлено. Настоящий рендеринг на GPU, настоящая JIT-компиляция и настоящее аппаратное декодирование несут в себе характерную стоимость — не единичное измерение, а распределение длительностей по множеству повторных операций, формируемое настоящим кремнием, который прогревает кэши и планирует работу по настоящим ядрам. Программный растеризатор, подменяющий GPU, или браузер, работающий внутри тяжёлой инструментации, склонны убедительно воспроизводить результат этой работы, но с трудом воспроизводят её распределение по времени — слишком равномерное, слишком быстрое или слишком медленное так, что не совпадает ни с одним настоящим устройством.
Это одно из сравнений доверенных и недоверенных источников, на которые опирается модель обнаружения лжи CreepJS: выполнить одно и то же вычисление в основном потоке и внутри Worker, или сравнить заявленную возможность с временной сигнатурой, которую эта возможность должна производить, и отметить случаи расхождения. Это также часть причины, по которой headless- и автоматизированные браузеры продолжают попадаться даже после того, как их очевидные приметы — отсутствующий флаг navigator.webdriver, подозрительный User-Agent — уже залатаны. Подделать форму значения достижимо. Подделать распределение по времени, которое производит это значение под огрублёнными, зашумлёнными часами, — гораздо более сложная цель, именно потому что часы, которые ловят подделку, — те же самые огрублённые и шумные часы, оставленные защитой от Spectre.
Цена: точность, которую не вернуть
Находка Cloudflare про Workers и огрубление performance.now() в браузере — один и тот же компромисс на разных участках стека: заморозить или огрубить часы, закрыть побочный канал и смириться с тем, что легитимные измерения станут шумнее в качестве побочного эффекта. Исследователи Cloudflare показали, что один сетевой запрос к внешним часам и обратно возвращает атакующему нужную ему точность. Страница, работающая в вашем браузере, такого обходного пути для собственных внутренних измерений не получает — любая длительность, которую скрипт измеряет через performance.now(), включая те, что стоят за сетевой диагностикой в браузере, наследует этот предел огрубления времени и весь джиттер, добавленный сверху.
Этот предел мал в абсолютных числах — около 100 микросекунд вне изоляции, ближе к 5 внутри неё, — что почти не имеет значения для всего, что измеряется целыми секундами. Но это ещё один источник шума от запуска к запуску, накладывающийся на изменчивость, и без того присущую измерению реального сетевого пути, — именно об этом со стороны сети подробно рассказывает наш материал почему результаты теста скорости каждый раз разные. Часы — не главный источник этого шума. Они просто никогда не бывают полностью тихими по замыслу — тому же самому замыслу, что не даёт странице считать ваш кэш побитово.
Часто задаваемые вопросы
Почему браузеры сделали performance.now() менее точным?
Чтобы закрыть побочный канал Spectre-класса. Атакам по времени через кэш нужны часы, достаточно точные, чтобы отличить попадание от промаха, — разница составляет всего наносекунды. Огрубление часов и добавление джиттера, как того требует алгоритм «огрубления времени» из спецификации W3C, делает это различие ненадёжным, не ломая таймер для обычного использования.
Значит ли это, что я не могу точно измерять производительность в браузере?
Не для чего-либо в масштабе, воспринимаемом человеком. Ограничение стоит вам примерно 100 микросекунд точности вне изоляции или около 5 внутри неё — незначительно для измерения сетевого запроса или прохода рендеринга, но вполне реально, если вы пытаетесь измерять отдельные обращения к памяти, — именно в этом и смысл ограничения.
Как узнать, что страница кросс-доменно изолирована?
Проверьте window.crossOriginIsolated в консоли — значение будет true только если страница отдала оба заголовка: Cross-Origin-Opener-Policy: same-origin и Cross-Origin-Embedder-Policy, согласно MDN. Большинство обычных страниц этим не занимаются, поскольку изоляция ограничивает, какой кросс-доменный контент они могут встраивать.
Меняет ли resistFingerprinting в Firefox точность таймера ещё сильнее?
Да. Поверх базового Spectre-огрубления, которое применяет каждый браузер, RFP округляет таймеры до более грубых, единообразных шагов именно для того, чтобы фингерпринтинг по джиттеру таймера давал меньше сигнала — тот же подход единообразия, который мы разбираем в подробном материале про RFP.


