Back

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

Безопасное программирование для 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 может с ним сделать?» Этот вопрос, применяемый последовательно, выявляет большинство проблем до их выпуска в продакшн.

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

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

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

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

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

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.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay