У стороннего кода были тяжелые несколько месяцев. 29 октября скрипт мониторинга производительности на сайте Polymarket был заменен вредоносным кодом после взлома DNS провайдера скрипта, и теперь Polymarket выплачивает трейдерам около 3,9 миллиона долларов. В период с августа по октябрь атаки типа «червь» также привели к распространению вредоносных версий доверенных пакетов npm, причем одна из волн затронула более 400 пакетов. В обоих случаях код из доверенного источника изменился, и каждый сайт, загружавший его, запускал новую версию с полным доступом к странице.
Проанализировав происходящее со скриптами на стороне браузера, мы захотели, чтобы через RudderStack запускался именно тот код, который мы протестировали, байт в байт. Когда служба безопасности проверяет SDK клиентских данных, она обычно одобряет его домен и переходит к другим задачам. Скрипты, которые SDK загружает впоследствии, обычно привлекают меньше внимания, и в этом посте мы как раз рассказываем о них.
Как проникает сторонний код
Каждый веб-сайт загружает код из нескольких источников, и каждый из них является источником, которому вы доверяете. В случае с RudderStack это включает устанавливаемый вами SDK и, при использовании дестинаций в режиме устройства, скрипты, которые провайдер каждого инструмента публикует для собственного продукта.
Существует два способа установки JavaScript SDK. При использовании пакета npm наш код объединяется в ваше приложение и обслуживается с вашего собственного домена. Для браузера это код первой стороны, и вы полностью контролируете, какая именно версия поставляется. С помощью CDN-сниппета вы доверяете одному домену RudderStack и той версии, которую он предоставляет.
Облачный режим против режима устройства. RudderStack может доставлять события в такой инструмент, как Amplitude, двумя способами. В облачном режиме SDK отправляет события в RudderStack, а наши серверы пересылают их в Amplitude, поэтому ничто из Amplitude не запускается на вашей странице. В режиме устройства SDK загружает собственный JavaScript SDK от Amplitude на вашу страницу и передает ему события в браузере. Команды выбирают режим устройства, когда инструмент должен работать в браузере, например для сессионных записей, тепловых карт или внутриприложений сообщений.
Для каждой интеграции в режиме устройства SDK запускает тонкую обертку, которая загружает собственный браузерный SDK инструмента (Amplitude, GA4, Mixpanel и другие) и пересылает ему ваши события. На сегодняшний день мы поддерживаем более 80 веб-интеграций в режиме устройства. Каждая из них извлекает скрипт из домена, контролируемого ее провайдером.
Каждый инструмент публикует свои скрипты совершенно по-разному:
- Точные версии, такие как /2.42.1/. Содержимое никогда не должно меняться.
- Вечнозеленые URL (evergreen), такие как /latest или /v2. Содержимое меняется при каждом обновлении от провайдера.
- URL, специфичные для учетной записи, в которые встраивается ваш проект или идентификатор учетной записи, часто вообще без указания версии.
Вечнозеленый URL означает, что любое изменение на стороне провайдера, включая вредоносное, попадает на ваш сайт при следующей загрузке страницы. В основном это веб-проблема. На мобильных устройствах владелец приложения добавляет каждый сторонний SDK в проект на этапе сборки и закрепляет его версию там, поэтому во время выполнения ничего не загружается.
При использовании вечнозеленого URL ваш сайт запускает то, что провайдер опубликовал совсем недавно, и никто с вашей стороны это не проверял. Мы хотели, чтобы скрипт на вашей странице был именно тем, который мы протестировали, и оставался таковым.
Что мы выпустили
Три изменения работают в комплексе. Точные версии фиксируют, какой именно релиз запускает ваш сайт, проверки целостности доказывают, что байты не изменились, а строгий режим не пропускает ничего, что мы не можем проверить.
Точные версии по умолчанию
11 из 83 защищенных на сегодняшний день (7 закрепленных с помощью SRI + 4 без стороннего скрипта) теперь загружают точный сторонний релиз вместо меняющегося: Bugsnag, Iterable, Keen и LaunchDarkly получают новые закрепления, а Amplitude, Rollbar и Sentry сохраняют уже использовавшиеся точные версии. Ваш сайт запускает протестированный нами релиз независимо от того, что провайдер выпустит позже.
Мы закрепляем интеграцию только тогда, когда выполняются все следующие условия:
- URL с точной версией существует на том же хосте, с которого интеграция загружается изначально, поэтому вам не нужны новые записи в политике защиты контента (CSP).
- Заголовки CORS хоста позволяют браузеру выполнять проверку целостности.
- Закрепление охватывает каждый сторонний скрипт, который интеграция добавляет на страницу.
- Каждая существующая функция интеграции продолжает работать.
Проверки целостности подресурсов (SRI)
Закрепленный URL по-прежнему указывает на сервер, управляемый провайдером. Поэтому для каждого закрепленного файла необходимо убедиться в совпадении байтов и сохранить его хэш SHA-384 в коде интеграции. CI повторно сверяет сохраненные хэши с актуальными файлами. SDK добавляет хэш в качестве атрибута целостности тега скрипта, и браузер отказывается запускать скрипт, если загруженные байты отличаются. Заблокированный скрипт влияет только на эту конкретную дестинацию; SDK и остальные ваши дестинации продолжают работать.
Строгий режим (опционально)
Для интеграций, которые мы не можем закрепить, поведение по умолчанию остается прежним: они загружаются с обычного URL провайдера. Строгий режим отменяет это поведение. Это одна глобальная опция загрузки:
JAVASCRIPT
rudderanalytics.load(WRITE_KEY, DATA_PLANE_URL, {
vendorSdkVersionPolicy: 'strict',
В строгом режиме интеграция, которую невозможно закрепить, останавливается до того, как ее сторонний скрипт будет добавлен на страницу. SDK записывает в лог Skipped <name>: its vendor script cannot be pinned (strict mode). и продолжает работу. Название опции и сообщение лога по-прежнему используют слово "vendor"; они ссылаются на те же самые сторонние скрипты.
Поведение в зависимости от типа интеграции
- Закреплено с помощью SRI (Amplitude, Bugsnag, Iterable, Keen, LaunchDarkly, Rollbar, Sentry): по умолчанию: точная версия + SRI; строгий режим: точная версия + SRI.
- Нет стороннего скрипта (Lotame, Ninetailed, Optimizely, Shynet): по умолчанию: загружается; строгий режим: загружается.
- Когда вы задаете URL скрипта (базовый URL для GA4, GA4 V2 и Google Ads SDK, URL нативного SDK Appcues, пользовательский домен Rockerbox, URL пользовательской библиотеки Mixpanel): по умолчанию: загружается с вашего URL; строгий режим: загружается с вашего URL. Без пользовательского URL они следуют последнему пункту: по умолчанию загружаются как обычно и пропускаются в строгом режиме.
- Сторонний скрипт, который невозможно закрепить: по умолчанию: обычный URL провайдера, без изменений; строгий режим: пропускается.
Строгий режим не применяется к пользовательским интеграциям, добавляемым с помощью addCustomIntegration, так как вы сами контролируете этот код.
Поддержание актуальности закреплений
При закреплении новый релиз провайдера попадает на ваш сайт только после того, как мы проверим его и включим в нашу интеграцию, поэтому мы стараемся сократить эту задержку.
Закрепления поставляются в составе наших бандлов интеграции, которые по умолчанию следуют версии вашего основного SDK. Если вы загружаете SDK по URL, который отслеживает релизы, новые закрепления попадают на ваш сайт при следующей загрузке страницы. Если вы закрепили SDK на точной версии, вы получаете их при обновлении.
С нашей стороны CI проверяет каждый сохраненный хэш, а существующий рабочий процесс отслеживает основные обновления сторонних скриптов. В дальнейшем мы автоматизируем патчевые и минорные обновления: система открывает пулл-реквест, сквозные тесты проверяют его, а дежурный инженер проверяет и объединяет его.
Мы намеренно оставили общедоступную настройку в виде одного переключателя. На сегодняшний день лишь некоторые провайдеры публикуют релизы, допускающие закрепление, поэтому выбор версий для каждой дестинации добавил бы слишком много настроек при минимальной пользе. Мы вернемся к этому вопросу по мере расширения покрытия.
Рекомендуемая нами конфигурация
Наиболее защищенная конфигурация загружает минимум кода с доменов, которые вы не контролируете. Наше руководство по укреплению защиты SDK уже охватывает большую часть этого, а строгий режим (strict mode) теперь добавляет усиленный вариант для команд, которым нужен режим устройства (device mode).
Каждая настройка начинается с одних и тех же двух шагов, а затем разделяется в зависимости от того, нужен ли вам режим устройства.
- Установите из npm с помощью имеющегося в бандле экспорта. Импортируйте из @rudderstack/analytics-js/bundled. Плагины поставляются внутри вашего бандла, поэтому SDK ничего не скачивает во время выполнения. Вы также можете самостоятельно хостить файлы плагинов и интеграций или раздавать SDK со своего собственного домена.
- Отключите полифилы. Установите polyfillIfRequired: false, чтобы SDK никогда не добавлял скрипт полифила со стороннего домена.
Затем выберите один из путей:
- Без режима устройства (наиболее защищенный вариант): сделайте мок конфигурации источника. Передайте функцию getSourceConfig, которая возвращает ваш источник с пустым списком назначений (destinations). SDK не делает запросов конфигурации и не загружает сторонние скрипты. Назначения в облачном режиме (cloud mode) по-прежнему получают каждое событие, поскольку RudderStack маршрутизирует их на сервере. В руководстве по укреплению защиты приведена полная конфигурация.
- Нужен режим устройства: включите строгий режим (strict mode). Сохраните реальную конфигурацию источника, так как она нужна SDK для загрузки назначений в режиме устройства. Установите vendorSdkVersionPolicy: 'strict', и SDK будет загружать только те сторонние скрипты, которые он может зафиксировать и проверить.
Путь с режимом устройства выглядит следующим образом:
JAVASCRIPT
import { RudderAnalytics } from '@rudderstack/analytics-js/bundled';
const rudderAnalytics = new RudderAnalytics();
rudderAnalytics.load(WRITE_KEY, DATA_PLANE_URL, {
polyfillIfRequired: false,
vendorSdkVersionPolicy: 'strict',
Используете CDN-сниппет? Зафиксируйте sdkVersion на точной версии релиза в сниппете. SDK определяет пути к плагинам и интеграциям на основе собственного URL, поэтому одно это изменение зафиксирует и их, если только вы не переопределите destSDKBaseURL или pluginsSDKBaseURL. Зафиксированный SDK не обновляется автоматически, поэтому следите за releases и регулярно обновляйтесь.
Что это пока не охватывает
Мы фиксируем каждую интеграцию, где провайдер предоставляет такую возможность и мы можем охватить всё, что она загружает. Этот список будет расширяться.
- Что доказывает хэш: SRI подтверждает, что вы запускаете именно тот файл, который мы протестировали. Это не доказывает, что сторонний код безопасен.
- Что охватывает SRI: код, который сторонний скрипт загружает позже, удаленные настройки и iframe остаются за рамками проверки. Некоторые интеграции загружают дополнительный код именно так, поэтому мы фиксируем их только тогда, когда сможем охватить всё, что они загружают. Это наш следующий этап.
- Скрипты, которые невозможно зафиксировать: некоторые провайдеры собирают скрипт индивидуально для каждого аккаунта клиента, а другие используют один URL без версионирования, который меняется на месте. Это справедливо независимо от того, как вы устанавливаете эти теги — с использованием платформы клиентских данных или без нее. Строгий режим позволяет исключить их вместо того, чтобы слепо им доверять.
- Скрипты, которые вы хостите сами: если вы настраиваете интеграцию на вашу собственную копию стороннего скрипта, мы загружаем его как есть. В будущем мы планируем добавить возможность указывать SRI-хэш для этого URL в панели управления.
Как начать
Точные версии и проверки SRI включены по умолчанию в последней версии JavaScript SDK (v3.36.0 или новее), поэтому для их получения достаточно просто обновиться. Строгий режим — это один из параметров загрузки. Оба подхода соответствуют рекомендациям из OWASP Third Party JavaScript Management Cheat Sheet, а исходный код SDK доступен на GitHub.
Для полной настройки, включая конфигурацию только для облачного режима и защищенный CDN-сниппет, см. руководство Harden the JavaScript SDK.
Спасибо Саи Кумару Баттиножу (Sai Kumar Battinoju) и команде SDK, которые спроектировали и выпустили это.












