12k
All articles

Безопасное программирование для JavaScript-разработчиков

Безопасный JavaScript для браузерных приложений: предотвращайте DOM XSS, избегайте eval(), используйте CSP, защищайте токены и проверяйте postMessage.

OpenReplay Team
OpenReplay Team
Безопасное программирование для JavaScript-разработчиков

JavaScript работает везде, где есть браузер, что делает его одной из наиболее атакуемых поверхностей в разработке программного обеспечения. Большинство уязвимостей возникают не из-за экзотических эксплойтов — они появляются из-за предсказуемых паттернов в повседневном коде. Это руководство охватывает практики безопасного программирования на JavaScript, которые наиболее важны, когда ваш код выполняется непосредственно в браузере.

Ключевые выводы

  • DOM-based XSS является одной из наиболее распространённых уязвимостей JavaScript — избегайте передачи недоверенных данных в такие приёмники (sinks), как innerHTML, eval() или document.write()
  • Используйте textContent вместо innerHTML для данных, предоставленных пользователем, и санитизируйте с помощью DOMPurify, когда действительно необходимо отрендерить HTML
  • Никогда не используйте eval(), Function() или строковые формы setTimeout/setInterval с динамическим вводом
  • Применяйте Content Security Policy (CSP) с nonce или хешами и комбинируйте с Trusted Types для эшелонированной защиты
  • Избегайте хранения токенов аутентификации или секретов в localStorage или sessionStorage — предпочитайте HttpOnly cookies для идентификаторов сессий
  • Всегда проверяйте event.origin при обработке событий postMessage
  • Регулярно проводите аудит дерева зависимостей и используйте Subresource Integrity для скриптов, загружаемых с CDN

DOM-Based XSS: одна из наиболее распространённых уязвимостей JavaScript

Предотвращение XSS в JavaScript начинается с понимания того, где она фактически возникает. DOM-based XSS происходит, когда ваш код читает из источника, контролируемого атакующим — например, location.hash, document.referrer или URLSearchParams — и небезопасно записывает это на страницу.

Основное правило: никогда не передавайте недоверенные данные в приёмник (sink), который интерпретирует HTML или выполняет код.

Небезопасные приёмники, которых следует избегать с динамическими данными:

// ❌ Все эти методы могут выполнить внедрённые скрипты
element.innerHTML = userInput
element.outerHTML = userInput
document.write(userInput)
eval(userInput)
setTimeout(userInput, 0)       // только строковая форма
new Function(userInput)()

Безопасные альтернативы:

// ✅ Обрабатывает содержимое как текст, никогда как разметку
element.textContent = userInput
element.innerText = userInput

Когда вам действительно необходимо отрендерить HTML — например, в редакторе форматированного текста — сначала санитизируйте данные с помощью DOMPurify:

// ✅ Безопасный рендеринг форматированного текста
element.innerHTML = DOMPurify.sanitize(userInput)

Та же логика применяется к setAttribute. Динамические атрибуты, такие как href, src и обработчики событий (onclick, onload), могут выполнять JavaScript. Придерживайтесь статических, не исполняемых атрибутов при работе с пользовательскими значениями.

Динамическое выполнение кода всегда небезопасно

eval(), Function() и строковые формы setTimeout/setInterval — это не просто плохая практика, они являются прямыми векторами внедрения кода. Не существует безопасного способа использовать их с недоверенным вводом.

Если вы парсите данные, используйте JSON.parse(). Если вам нужно динамическое поведение, используйте структуры данных и явную логику вместо генерации кода во время выполнения.

Content Security Policy как уровень защиты

Content Security Policy (CSP) ограничивает, какие скрипты могут выполняться и откуда они могут загружаться. Строгая CSP, использующая nonce или хеши — а не 'unsafe-inline' — значительно уменьшает радиус поражения любой XSS, которая проскользнула через защиту.

Комбинируйте CSP с Trusted Types в поддерживающих браузерах для обеспечения безопасной записи в DOM на уровне API. Поддержка Trusted Types браузерами продолжает расширяться, и её можно отслеживать на webstatus.dev.

Безопасные API браузера: cookies и клиентское хранилище

Cookies, содержащие идентификаторы сессий, всегда должны иметь флаги HttpOnly (блокирует доступ из JavaScript), Secure (только HTTPS) и SameSite=Strict или Lax (помогает предотвратить CSRF). Установка этих флагов на стороне сервера более надёжна, чем из JavaScript.

localStorage и sessionStorage доступны любому скрипту на странице. Избегайте хранения там токенов аутентификации, секретов сессий или конфиденциальных пользовательских данных — уязвимость XSS немедленно раскроет всё содержимое хранилища.

Межсайтовый обмен сообщениями с postMessage

postMessage полезен, но его легко использовать неправильно. Всегда проверяйте origin входящих сообщений перед обработкой их данных:

window.addEventListener('message', (event) => {
  // ✅ Всегда проверяйте origin перед обработкой
  if (event.origin !== 'https://trusted-origin.com') return

  handleMessage(event.data)
})

При отправке сообщений избегайте использования '*' в качестве targetOrigin, если у вас действительно нет фиксированного получателя. На принимающей стороне всегда проверяйте event.origin, чтобы убедиться, что сообщение пришло с доверенного сайта. Более подробная информация о безопасном использовании приведена в документации postMessage.

Безопасность цепочки поставок JavaScript

Безопасность цепочки поставок JavaScript — растущая проблема. Один скомпрометированный или вредоносный пакет может затронуть тысячи приложений. Практические шаги:

  • Запускайте npm audit или используйте Snyk для обнаружения известных уязвимостей в зависимостях
  • Коммитьте ваш lockfile (package-lock.json или yarn.lock) и относитесь к неожиданным изменениям как к тревожному сигналу
  • Используйте хеши Subresource Integrity (SRI) для всех скриптов, загружаемых с CDN
  • Проводите аудит новых пакетов перед их добавлением — проверяйте количество загрузок, активность поддержки и не является ли название пакета опечаткой (typosquat)

Краткий справочник: безопасные и небезопасные паттерны

НебезопасноБезопасная альтернатива
innerHTML = userInputtextContent = userInput
eval(str)JSON.parse(str)
setTimeout(str, n)setTimeout(fn, n)
Токен в localStorageHttpOnly cookie
message без проверки originПроверяйте event.origin сначала

Заключение

Большинство уязвимостей JavaScript следуют одному и тому же паттерну: недоверенные данные достигают мощного API без валидации. Выработайте привычку задавать вопрос: «откуда берётся это значение и что этот API может с ним сделать?» Этот вопрос, применяемый последовательно, выявляет большинство проблем до их выпуска в продакшн.

Часто задаваемые вопросы

Всегда ли небезопасно использовать innerHTML в JavaScript?

Не всегда, но это небезопасно, когда вы передаёте ему данные, которые происходят из пользовательского ввода или любого внешнего источника. Если вам необходимо отрендерить динамический HTML, сначала санитизируйте его с помощью библиотеки типа DOMPurify. Для простого текстового содержимого используйте textContent, который никогда не интерпретирует разметку и не выполняет скрипты.

Почему не следует хранить JWT-токены в localStorage?

localStorage доступен любому JavaScript, выполняющемуся на странице. Если атакующий эксплуатирует уязвимость XSS, он может прочитать всё содержимое хранилища, включая ваши токены. HttpOnly cookies — более безопасный выбор, потому что JavaScript вообще не может получить к ним доступ, что ограничивает ущерб от клиентских атак.

В чём разница между DOM-based XSS и reflected XSS?

Reflected XSS включает вредоносную нагрузку, отправленную на сервер и возвращённую обратно в ответе. DOM-based XSS никогда не достигает сервера. Вместо этого клиентский JavaScript читает из источника, контролируемого атакующим, такого как фрагмент URL, и небезопасно записывает его на страницу. Оба типа опасны, но DOM-based XSS сложнее обнаружить на стороне сервера.

Как Content Security Policy предотвращает XSS-атаки?

CSP сообщает браузеру, каким источникам скриптов разрешено выполняться. Строгая политика, использующая nonce или хеши, блокирует встроенные скрипты и несанкционированные внешние скрипты от выполнения. Даже если атакующий внедрит вредоносную разметку на страницу, браузер откажется её выполнять, потому что она не соответствует политике.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.