dimhold.by
← Статьи

Source maps в проде. 55 сайтов из топ-1000 отдают свой код

В ноябре прошлого года Apple запустила новую веб-версию App Store с включёнными source maps. Через несколько часов весь фронтенд на Svelte и TypeScript лежал на GitHub. Никто ничего не взламывал. Разработчик, который его выложил, воспользовался расширением для браузера, которое сохраняет все ресурсы страницы. Карты просто оказались среди них. Сначала я подумал, что это разовый промах одной команды. Я был уверен, что продовая сборка у любой серьёзной компании выкидывает карты или хотя бы не кладёт их на публичный сервер. Эту уверенность я никогда не проверял, так что теперь решил посчитать.

Source map (карта исходников) это JSON-файл, который бандлер пишет рядом с собранным JavaScript. По нему браузер показывает настоящие имена файлов и номера строк, когда что-то падает. В последней строке собранного файла указана его карта, //# sourceMappingURL=app.js.map. Если в карте есть ещё и поле sourcesContent, в нём лежит полный текст каждого исходного файла. Тогда любой с curl восстановит фронтенд целиком, с папками и внутренними именами.

тот .js, который выполняет браузер function t(e){…}(); //# sourceMappingURL=app.js.map тот .map, на который он указывает { "sources": [ "…/featureFlag.ts" ], "sourcesContent": [ "export function…" ] } и вот исходный файл export function checkFeatureFlag(k) { return flags[k] ?? false } скачать sourcesContent
Собранный бандл сам указывает на свою карту. Скачиваешь карту, читаешь sourcesContent и исходный файл возвращается с настоящими именами.

Устроен замер просто. Я взял первые 1000 доменов списка Tranco K25GW. Это закреплённый снимок 2023 года, так что прогон можно повторить на том же списке. Потом написал на Python 3.12 краулер примерно в 160 строк, с requests 2.31 и BeautifulSoup 4.15. Для каждого домена он загружает главную страницу и берёт до 12 внешних скриптов. В каждом скрипте ищет sourceMappingURL, скачивает карту и смотрит, есть ли что-нибудь в sourcesContent. Прогон занимает около 4,5 минуты в 24 потока. Я сохранял пути файлов, а их содержимое не сохранял.

Из 1000 доменов страницей ответили 686. Внешний JavaScript отдал 581 из них. У 206 нашёлся хотя бы один sourceMappingURL, это 30 процентов ответивших. В HTTP Archive в 2019 году насчитали по своему обходу от 17 до 18 процентов. Они считали только скрипты с домена самого сайта, а я считал все скрипты на странице, так что это разные замеры. 87 сайтов отдали мне карту с исходным текстом внутри. Некоторые карты отдаются под несколькими доменами одного владельца. Если считать каждую один раз, получается 199 карт, вместе около 182 мегабайт исходного текста в 35 466 файлах.

ответили 686 отдали JS 581 есть ссылка на карту 206 отдали исходник 87 свой код 55
Из 686 ответивших сайтов у 206 есть sourceMappingURL и 87 отдали карту с исходником внутри. После ручного разбора путей свой код отдают 55 из них.

Где мой счёт развалился

Число 87 просилось в заголовок, но было неверным. В первой карте Trello, которую я открыл, были файлы с именами <anon> и <prelude>, а это служебная обвязка бандлера. Тогда я добавил правило. Сайт сливает свой код, если в карте есть файлы в папках src/, app/, components/, pages/ или lib/. Вышло 77. Потом я заметил, что правило ловит и такие папки внутри node_modules. Когда я выкинул их вместе с вендорными папками и публичными CDN, осталось 67. Я чуть не написал 67 в этот текст.

Затем я прочитал пути этих 67 сайт за сайтом. Правило не выдержало. Среди “своих” файлов Coursera были ../../src/client.ts и ../../src/eventbuilder.ts, отданные с browser.sentry-cdn.com. Это Sentry SDK со своей папкой src. Свой код у Coursera в других картах есть, но эти 2 файла к нему не относятся. У Вашингтонского университета были только Bootstrap и Popper. Mirror.co.uk отдал клиентскую часть самого Next.js. EPA и NOAA отдали по 1 файлу src/index.js. Он взят из маленькой библиотеки, которая входит в ядро Drupal.

Поэтому я разобрал все 87 руками. Смотрел на хост, с которого отдана карта, а ещё на то, чей код лежит по этим путям. Вердикты лежат в файле рядом с краулером, по строке на сайт, чтобы потом можно было поспорить с самим собой. При втором проходе 6 сайтов переехали в другие группы, так что граница между ними зыбкая.

  • 55 сайтов отдают карты со своим кодом, со своего домена или своего CDN.
  • 19 сайтов текут только через чужой встроенный скрипт. Это CDN Sentry у HBO Max и savefrom.net, скрипт мониторинга SpeedCurve у LA Times и Nikkei, виджет Trustpilot, меню HubSpot и рекламные скрипты.
  • 13 сайтов показывают только публичные библиотеки с открытым кодом или код фреймворка.
55 свой код, свой хост 19 только чужой встроенный скрипт 13 только открытые библиотеки
Те же 87 я разобрал вручную по хосту карты и путям файлов. Если считать по владельцам, 55 сайтов со своим кодом принадлежат 47 владельцам.

У некоторых из 55 общий владелец. Atlassian отдаёт один и тот же бандл на 3 своих доменах, среди них bitbucket.org, около 3400 файлов с путями вроде services/wac-web/src/utils/feature-flags/check-feature-flag.ts. Карты Trello приходят с той же инфраструктуры Atlassian. BuzzFeed и HuffPost используют общие приватные пакеты @buzzfeed. По владельцам выходит 47. 55 сайтов это 8,0 процента ответивших.

Кто это

Среди них GitHub, Atlassian, Trello, Goodreads, Eventbrite, Wix, Tencent, JD.com, Nvidia, Siemens, AMD. Есть там и Red Hat, Daily Mail, Guardian, Deutsche Welle, weather.com и несколько университетов. В картах GitHub с githubassets.com 786 файлов, с путями вроде packages/client-env/client-env.ts из их монорепозитория. В путях Daily Mail до сих пор видна рабочая папка агента сборки TeamCity. Это большие компании со своей инфраструктурой сборки, агентами TeamCity и монорепозиториями, но как карты попали на публичный сервер, я не знаю ни про одну из них. Моя догадка обычная: карту собирают для трекера ошибок, а потом она уезжает в деплой вместе с бандлом. Но я этого не проверял.

Намерение краулер тоже не читает. freeCodeCamp и WordPress отдают карты кода, который и так открыт. У них это может быть осознанным выбором. С сайтом Bootstrap то же самое. В картах Font Awesome лежит исходник их платных пакетов иконок Pro. Часть остальных, наверное, тоже решила, что фронтенд не секрет, а это разумная позиция. Но в 55 смешаны оба случая, а снаружи я их не разделю.

По ответу сервера их тоже не отличить. Утёкшие карты приходили с 7 разными типами содержимого, от application/json до text/plain, а одна вообще без типа. Ни одной не понадобились куки или вход в аккаунт.

Чего я не проверял

Я смотрел только главную страницу и не больше 12 скриптов на сайт. Внутренние страницы и чанки с ленивой загрузкой я не трогал, так что 55 это нижняя граница. За логин тоже не заходил. Внутри файлов я не искал ключи и токены, потому что мне нужен был размер утечки. Обход я запускал один раз с одной машины. Сайт может выключить свои карты в любой день.

Проверка своих деплоев занимает один запрос. Взять любой скрипт с продовой страницы и прочитать его последнюю строку. Если там есть sourceMappingURL, попробовать скачать карту. 404 или карта без sourcesContent это нормально. Если в sourcesContent есть текст, мой краулер посчитал бы и этот сайт.