Anubis потратил год на WebAssembly-защиту от ИИ-скрейперов

Xe Iaso, автор и CEO компании Techaro, разработчика анти-скрейпингового инструмента Anubis, опубликовал 6 сентября 2026 года подробный разбор того, как за год работы (сотни коммитов, пять поколений пул-реквестов, десятки тестов, частичный переход на Rust, первый в карьере баг компилятора и минимум три раза выведенная из строя нехваткой памяти домашняя рабочая станция) в Anubis появились проверки на основе WebAssembly.

Anubis защищает сайты от массового скрейпинга ИИ-компаний: перед доступом к странице показывает вызов, требующий вычислительной работы (proof-of-work), на единичный запрос нагрузка незаметна, а массовый обход скрейпером становится дорогим. Новая функция, которую администраторы смогут включить в правилах порогов (challenge: algorithm: argon2id), переводит эти вычисления на WebAssembly и меняет алгоритм на argon2id, memory-hard, то есть требовательный не только к процессору, но и к памяти. По словам автора, это должно фактически похоронить путь «попросить нейросеть на скорую руку собрать CUDA-решатель под Anubis»: memory-hard задачу так просто GPU-ускорением не обойти.

Главная причина перехода на WebAssembly, баланс между слабыми процессорами телефонов и мощными процессорами скрейперов: снижать сложность для телефонов, не давая при этом атакующим подсказки, как снизить её и для себя, сложно. WebAssembly-бинарник исполняется быстрее, значит проверка на легитимном устройстве завершается быстрее.

Ключевое архитектурное решение, один и тот же бинарник (не просто одинаковый код) выполняется и в браузере клиента, и на сервере, что держит их в жёсткой синхронизации, автор сравнивает это с чипом CIC в приставке NES. У этого есть теоретический риск: ошибка в общем коде может привести к тому, что сервер примет как валидное решение, которое клиент на самом деле не должен был подобрать, но пока это не считается практической проблемой.

Один бинарник для клиента и сервера также открывает архитектурный путь к разбиению Anubis на плагины, которые можно будет обновлять и подгружать по отдельности, не пересобирая весь проект. При этом сам Anubis остаётся на Go, переписывать его на Rust целиком автор не планирует; на Rust (в конфигурации no_std, wasm32-unknown-unknown) написаны только сами WebAssembly-модули, потому что Rust даёт самые компактные и быстрые WebAssembly-сборки.

Попутно исправлена математическая ошибка в старом «быстром» челлендже: он считал количество нулевых полубайтов (nibble, по 4 бита) в хеше вместо количества нулевых бит, из-за чего увеличение сложности на единицу в худшем случае делало задачу в 16 раз тяжелее вместо ожидаемого прироста.

Большая часть года ушла на совместимость с браузерами. Anubis обязан поддерживать Chrome начиная с версии 75, из-за смартфонов и смарт-телевизоров на Android, которые физически не могут обновиться; JavaScript-запасной вариант для надёжности целится ещё ниже, в Chrome 66. Поддержка SIMD в WebAssembly (базовая с Chrome 91) собирается отдельной сборкой, и в браузере через библиотеку wasm-feature-detect автоматически выбирается версия с SIMD или без него.

Для клиентов, где WebAssembly отключён политикой, а JavaScript разрешён (например, режим Lockdown на iOS и настройки браузера Vanadium в GrapheneOS по умолчанию), вместо второй ручной реализации проверки автор использовал инструмент wasm2js из проекта Binaryen, он компилирует WebAssembly-модуль в JavaScript. Минус подхода, сгенерированный код заметно больше и медленнее исходного WebAssembly.

При сборке в GitHub Actions версия wasm2js из пакета Ubuntu падала с непонятной ошибкой про расширение tail call, тогда как версия из Fedora работала. Бисекцией версий Binaryen автор дошёл до сборки 128 (на тот момент, самой новой), которой не было в большинстве дистрибутивов. Вместо того чтобы требовать от всех конкретную системную версию, он скомпилировал сами инструменты wasm2js и wasm-opt в WebAssembly и закоммитил детерминированную сборку в репозиторий, так у всех, кто собирает Anubis, инструменты гарантированно совпадают с авторскими.

В процессе обнаружился недетерминизм: каждая сборка отличалась от предыдущей примерно на 29 байт. Причина оказалась багом в LLVM, компилятор обходил блоки обработки исключений в порядке машинных указателей, из-за чего порядок менялся от сборки к сборке. Это первый настоящий баг компилятора за карьеру автора; отключение рандомизации адресного пространства (ASLR) давало стабильный результат в рамках одной загрузки системы, что и навело на мысль, что дело не во входных данных, а в самом компиляторе. После того как баг починили в LLVM и вышла обновлённая версия wasi-sdk с фиксом, сборка WebAssembly в CI стала идти через wasmtime (с wazero как более медленным запасным вариантом) и всегда использует закоммиченные в репозиторий версии wasm-opt и wasm2js, ради побайтовой воспроизводимости.

Для тестирования в браузерах автор написал харнесс «chromesweep», который параллельно поднимает множество версий Google Chrome в конфигурации по умолчанию и прогоняет их против тестового экземпляра Anubis по HTTPS; библиотека версий браузеров выложена на GitHub как TecharoHQ/gubal. Старые версии Chrome он называет «действующе радиоактивными» с точки зрения безопасности и держит их в микровиртуальных машинах на Kata containers, изолированных строгой сетевой политикой Kubernetes, разрешающей доступ только к тестируемому Anubis и DNS. Проверка в браузерах подключена к отдельной slash-команде в pull request на GitHub.

Именно так нашлась ещё одна проблема: Chrome 75 (а также все версии вплоть до Chrome 100) выдавал ошибку компиляции WebAssembly в функции, связанной со стандартной библиотекой Rust (предположительно вокруг std::sync::Once). Причина, предкомпилированная стандартная библиотека Rust, которую rustup скачивает уже собранной под все возможные функции WebAssembly, включая типы-ссылки за пределами базового MVP-набора; в отличие от Go, где стандартная библиотека и рантайм пересобираются при каждой кросс-компиляции. Решение, прогонять сборку через wasm-opt, вырезающий из WebAssembly-модуля возможности сверх MVP. Чтобы подобрать точный набор флагов wasm-opt, под который парсится Chrome 75, автор настроил цикл фаззинга на связке Claude Opus и GLM 5.2. Итоговый набор флагов опирается на даты появления функций в Chrome, указанные прямо в комментарии к сборочному скрипту: sign-ext и mutable-globals, с Chrome 74, bulk-memory и nontrapping-float-to-int, с 75-й версии, multivalue, с 85-й, SIMD, с 91-й.

Функция выходит выключенной по умолчанию в Anubis v1.28.0 (кодовое имя «Wuk Lamat»); включить её по умолчанию в v1.29.0 автор планирует по итогам обратной связи от администраторов и пользователей, конкретной даты для этого в посте нет.

Автор прямо перечисляет, что осталось незакрытым: в JavaScript-запасном варианте (через wasm2js) пока не подключён прогресс-бар, потому что не удалось корректно связать импорт update_nonce; новая WebAssembly-сборка настолько быстрее старой, что администраторам, вероятно, придётся заново поднимать значение сложности; отдельно ведётся работа над классификатором мобильного трафика по репутации IP-адреса и TLS-отпечаткам, чтобы точнее давать поблажку телефонам; в долгосрочных планах, научиться подменять модули проверки в рантайме, а не только при сборке, вплоть до загрузки WebAssembly-модулей из OCI/Docker-реестров.

Отдельно автор оговаривает роль ИИ в самой публикации: текст поста написан без ИИ (черновик велся в Google Docs, где видна вся история набора текста), а единственные два случая использования ИИ, помощь Claude Opus в отрисовке визуальной диаграммы битов/полубайтов и упомянутый выше цикл фаззинга Claude Opus / GLM 5.2 для подбора флагов сборки.

Ключевые факты

  • Anubis, инструмент защиты сайтов от массового скрейпинга ИИ-компаний через proof-of-work, получает WebAssembly-проверки: один и тот же Rust-бинарник исполняется и в браузере, и на сервере, алгоритм меняется на memory-hard argon2id вместо чисто CPU-нагрузки.
  • Заодно исправлена математическая ошибка старого челленджа: он считал нулевые полубайты вместо бит, из-за чего +1 к сложности в худшем случае утяжелял задачу в 16 раз.
  • Для совместимости с Chrome 75+ и клиентами, где отключён только WebAssembly (Lockdown-режим iOS, GrapheneOS Vanadium), тот же код компилируется в JavaScript через wasm2js из Binaryen.
  • По пути найден первый в карьере автора баг компилятора: недетерминизм сборки на ~29 байт из-за порядка обхода блоков обработки исключений в LLVM, фикс вошёл в обновлённый wasi-sdk.
  • Функция выходит выключенной по умолчанию в Anubis v1.28.0, включить по умолчанию планируют в v1.29.0 по фидбэку; ИИ использовался только для диаграммы и для фаззинга флагов сборки, не для текста поста.

Почему это важно

Это редкий по детальности публичный отчёт о годе инженерной работы над защитой от скрейпинга, темой, которая обычно скрыта внутри компаний. Anubis используют многие сайты как барьер против агрессивного сбора данных ИИ-компаниями, и переход на memory-hard WebAssembly-проверки, это конкретный технический ответ на конкретную атаку (GPU-ускоренные решатели), а не маркетинговое заявление.

Кому это важно

Администраторам сайтов, уже использующим или рассматривающим Anubis для защиты от скрейперов; инженерам, которые пишут аналогичные anti-bot и rate-limiting системы; разработчикам, работающим с кросс-платформенным WebAssembly и сталкивающимся с проблемами совместимости старых браузеров; всем, кому интересна живая история поиска бага в LLVM.

Как это применить

Проверки на WebAssembly появляются в Anubis v1.28.0, но выключены по умолчанию, включаются вручную через конфигурацию порогов, например challenge: algorithm: argon2id, difficulty: 6. Администраторам, которые включат функцию заранее, стоит быть готовыми поднять значение сложности: по словам автора, новая сборка работает значительно быстрее старой. Автор также просит присылать данные о производительности на слабых устройствах, чтобы точнее настроить баланс между защитой и удобством для мобильных пользователей.

Можно ли доверять

Источник, личный техноблог Xe Iaso, автора и CEO Techaro, компании-разработчика Anubis: это первичный источник от человека, лично написавшего код и явно указавшего пределы своих знаний (например, признание, что не до конца проследил, какая именно функция стандартной библиотеки Rust вызывала сбой). Одновременно это материал о собственном продукте автора, то есть не независимая проверка.

Риски и подводные камни

Автор сам перечисляет незакрытые проблемы: в JavaScript-запасном варианте не работает индикатор прогресса; из-за возросшей скорости проверки администраторам, вероятно, придётся пересчитывать сложность; классификатор мобильного трафика ещё в разработке. Отдельный архитектурный риск, то, что клиент и сервер используют один и тот же бинарник: ошибка в общем коде теоретически может заставить сервер принять как валидный ответ, который клиент не должен был суметь подобрать; практической проблемой это пока не стало, но и не исключено.

«У меня нет намерения переписывать Anubis на Rust или делать что-то настолько радикальное.»

— Xe Iaso, автор поста и CEO Techaro