Вход на сайт

Просмотр новости

Найдите то, что Вас интересует

Картинка грузится, а fetch к тому же хосту — нет: как CSP тихо сломала три интеграции

Дата публикации: 04-08-2026 10:15:55

Полгода назад я закрыл замечание аудита «на сайте нет CSP» — прописал
Content‑Security‑Policy с белым списком хостов, проверил, что ничего не
отвалилось, и забыл. За три дня на прошлой неделе я нашёл три интеграции,
которые эта политика убила молча. Ни одна из них не оставила следа на сервере.Страница, которая называет себя «в реальном времени», показывала одно и то же
число десять секунд подряд — и показывала так всем, с того самого дня, как я
включил CSP.Это заметка про то, почему такие поломки не видно, и про скрипт, который
находит их все сразу. Читать далее

Основное содержимое страницы с новостью.

Полгода назад я закрыл замечание аудита «на сайте нет CSP» — прописал
Content-Security-Policy с белым списком хостов, проверил, что ничего
не отвалилось, и забыл. За три дня на прошлой неделе я нашёл три интеграции, которые эта
политика убила молча. Ни одна из них не оставила следа на сервере.

Это заметка про то, почему такие поломки не видно, и про пятистрочный скрипт,
который находит их все сразу.

Симптом

У меня есть страница «МКС онлайн»: живая позиция станции — широта, долгота,
высота, скорость. Позиция считается в браузере: SGP4 по свежим элементам NORAD,
пересчёт раз в секунду.

Пользователь пожаловался, что карта пустая. Я открыл страницу и снял три замера
подряд:

  t+1s :  пикселей 99  |  широта 42,71°                                                                                                                      
  t+4s :  пикселей 99  |  широта 42,71°                                                                                                                      
  t+10s:  пикселей 99  |  широта 42,71°                                                                                                                      

Число не менялось. Страница, которая называет себя «в реальном времени», стояла
намертво — и стояла так у всех, с того самого дня, как я включил CSP.

Причина

В консоли:

  Refused to connect to 'https://celestrak.org/NORAD/elements/gp.php?CATNR=25544'                                                                            
  because it violates the Content Security Policy directive: "connect-src ..."                                                                               

connect-src не содержал celestrak.org. Значит fetch за TLE отклонялся,
satrec никогда не создавался, цикл на 1 Гц не запускался ни разу.

Дальше я проверил остальные внешние хосты и нашёл ещё два: ipwho.is
(IP → город, из этого строится «ближайший пролёт рядом с вами») и CDN NOAA,
откуда берётся снимок Земли с погодного спутника.

Почему это не видно

Три причины складываются в идеальную слепую зону.

1. img-src и connect-src — разные директивы, и первая обычно шире.

  img-src 'self' data: blob: https:                                                                                                                          
  connect-src 'self' https://us.i.posthog.com https://api.stripe.com ...                                                                                     

С такой политикой картинка с cdn.star.nesdis.noaa.gov грузится прекрасно.
А fetch к тому же самому хосту — чтобы прочитать заголовок Last-Modified и
показать время съёмки — отклоняется. Изображение на экране есть, подпись под ним
пустая. Выглядит как мелкий баг вёрстки, а не как заблокированный запрос.

2. Нарушение CSP происходит в браузере. На сервере его нет.

Мой хостинг вообще не логирует HTTP‑запросы: из ста строк лога ровно одна
содержала статус. Запрос, который браузер не отправил, не оставит следа нигде,
кроме консоли конкретного пользователя.

3. Мониторинг смотрел не туда.

Мой сторож грепал последние 100 строк лога — это около 56 минут на боевом
сервисе. Ошибка, которая случается раз в сутки, попадает в это окно примерно в
4% случаев. А фронтовые исключения я собирал через $exception — но
заблокированный CSP fetch не бросает необработанное исключение, если код
обёрнут в try/catch. А он был обёрнут: «на случай, если сеть недоступна».

Именно поэтому все три интеграции деградировали ровно так, как я их и написал —
аккуратно. Карта показывала пустое поле вместо позиции. Подпись под снимком
показывала пустоту вместо времени. Геолокация просила ввести город руками.
Ни одного алерта. Код вёл себя правильно; неправильной была моя уверенность,
что запрос вообще уходит.

Что с этим делать

Не «добавить хост» — это лечение симптома. Проверять весь список сразу.

Скрипт вытаскивает все https‑хосты, к которым обращается клиентский код, и
сверяет их с connect-src:

const csp = fs.readFileSync("next.config.ts", "utf8").match(/"connect-src[^"]*"/)[0];                                                                          
const allowed = [...csp.matchAll(/https:\/\/([a-z0-9.*-]+)/g)].map(m => m[1]);                                                                                 
                                                                                                                                                               
const hosts = new Set();                                                                                                                                       
for (const file of walk("components")) {                                                                                                                       
  const src = fs.readFileSync(file, "utf8");                                                                                                                   
  if (!/"use client"/.test(src.slice(0, 200))) continue;   // только клиентский код                                                                            
  for (const m of src.matchAll(/fetch\(\s*[`"']https:\/\/([a-z0-9.-]+)/g)) hosts.add(m[1]);                                                                    
  for (const m of src.matchAll(/URL\s*=\s*[`"']https:\/\/([a-z0-9.-]+)/g)) hosts.add(m[1]);                                                                    
}                                                                                                                                                              
                                                                                                                                                               
for (const h of hosts) {                                                                                                                                       
  const ok = allowed.some(a => a === h || (a.startsWith("*.") && h.endsWith(a.slice(1))));                                                                     
  if (!ok) console.log("БЛОКИРУЕТСЯ:", h);                                                                                                                     
}                                                                                                                                                              
                                                                                                                                                               
       

Тридцать секунд работы — и он нашёл третий хост, про который я ещё не знал.

Две вещи, которые я поменял насовсем:
  • report‑uri / report‑to в CSP. Браузер сам расскажет о нарушениях,
    и это единственный канал, который вообще существует для таких ошибок.

    • Тихий catch вокруг внешнего запроса — теперь повод остановиться и подумать. Он превращает отказ в пустое поле. Иногда это правильно; но тогда нужно, чтобы где‑то это фиксировалось.

    Вывод

    CSP не ломает страницу — она отнимает у неё функции. Разница в том, что упавшую
    страницу видно сразу, а страница без функции выглядит просто скучной. Мою
    «позицию в реальном времени» никто не считал сломанной: она показывала число.
    Просто всегда одно и то же.

    Если у вас есть CSP и есть клиентские fetch — прогоните скрипт. Тридцать
    секунд.

Схожие новости

#Наименование новостиТональностьИнформативностьДата публикации
1Почему сайт невидим для роботов: проблемы вайб-кодинга09.3624-07-2026
2Доверие корневому сертификату с ограниченным списком хостов в Chrome08.804-08-2026
3Чат на сайте стоит дороже, чем кажется: как я замерил его цену и переписал виджет под результат09.5828-07-2026
4Ты не найдёшь эту ошибку. Потому что её нет в твоём коде. Как Self-describing API спасает от чужих рефакторингов5807-07-2026
5Реальная и мнимая многопоточность в JS05.8330-07-2026
6TeamPCP: как команда хакеров-любителей «Дюны» закинула в наши Node.js-пакеты червей Shai-Hulud0829-06-2026
7Сложные сайты на вайбкоде: четыре лендинга, два пути к «дорогим» эффектам и 3D-логотип из спрайтов09.7705-08-2026
8Что сломается в вашем сервисе завтра, если сегодня вы запустите контейнеры без проверки образов06.3724-07-2026
9Spring Security: аутентификация через REST0507-07-2026
10Мониторинг модулей ядра и защита от Dirty-уязвимостей с помощью Wazuh0714-07-2026

Классификация: Мнения. Схожих патентов: 0. Схожих новостей: 10. Тональность: 0. Информативность: 7.2. Источник: habr.com.