JavaScript способен использоваться в атаках на пользователей, но современный браузер намеренно отделяет веб-страницу от операционной системы и ограничивает доступ к файлам, процессам, устройствам и данным других сайтов. Поэтому опасность обычно возникает не из самого языка, а из уязвимостей веб-приложения, ошибочной выдачи разрешений, вредоносных расширений, интеграции с локальными программами или ошибок безопасности самого браузера.
В этой статье разберём, какие атаки действительно возможны через JavaScript, как злоумышленник пытается приблизиться к пользовательскому ПК и почему сценарий «запустил JS на сайте — получил полный контроль над Windows» в нормальных условиях не работает.
Отдельно рассмотрим безопасные учебные примеры XSS, DOM XSS, postMessage, небезопасного использования DOM API и способы построения лаборатории для анализа подобных атак без обхода защит браузера.
Что вообще может сделать JavaScript из браузера
Чтобы правильно изучать браузерные атаки, сначала нужно разделить три разных уровня:
- JavaScript внутри веб-страницы.
- Возможности самого браузера.
- Операционная система пользователя.
Обычный скрипт относится к первому уровню. Он выполняется внутри изолированного контекста браузера и получает только те возможности, которые браузер сознательно предоставляет странице.
Это принципиальная граница безопасности. Веб-страница может изменять собственный DOM, выполнять сетевые запросы в рамках разрешённых браузером правил, использовать локальное хранилище своего происхождения, работать с некоторыми устройствами после выдачи разрешения и взаимодействовать с API самого браузера.
Но обычный JavaScript не получает универсального доступа к диску, не может перечислить произвольные процессы Windows, прочитать память другого приложения, выполнить команду cmd.exe или получить произвольный файл с диска только потому, что пользователь открыл страницу.
Поэтому фраза «взлом ПК через JavaScript» технически слишком широкая. Реальный сценарий обычно выглядит как цепочка:
уязвимость веб-приложения → выполнение JavaScript → получение доступа к доступным данным →
→ злоупотребление разрешениями или интеграциями → дальнейшая атака.
Если на каком-то этапе появляется настоящий контроль над операционной системой, значит между веб-страницей и ОС существует дополнительный механизм либо используется уязвимость самого браузера или подключённого ПО.
Почему браузер не даёт JavaScript полный доступ к компьютеру
Главная идея веб-безопасности заключается в изоляции происхождений и ресурсов.
Для браузера имеет огромное значение понятие origin — комбинация схемы, хоста и порта. Скрипт с одного origin не должен произвольно читать содержимое другого origin.
Например, страница:
https://example.com
не получает автоматический доступ к внутреннему DOM:
https://mail.example.net
только потому, что пользователь одновременно авторизован там.
Это фундаментальное ограничение называется Same-Origin Policy.
Без него практически любой вредоносный сайт мог бы открывать банковские страницы, корпоративные панели или веб-почту и читать информацию из них от имени пользователя.
При этом Same-Origin Policy не означает, что сайты вообще не могут взаимодействовать друг с другом. Браузер поддерживает CORS, postMessage, iframe, редиректы и множество других механизмов. Проблема возникает тогда, когда разработчик неправильно использует эти механизмы.
Самый важный класс: XSS
XSS, или Cross-Site Scripting, возникает тогда, когда атакующий получает возможность заставить приложение интерпретировать контролируемые им данные как HTML или JavaScript.
Для обучения особенно полезно различать:
- Stored XSS;
- Reflected XSS;
- DOM XSS.
С точки зрения браузера итоговая проблема похожа: JavaScript начинает выполняться в контексте доверенного сайта.
Именно здесь становится понятна настоящая опасность XSS.
Допустим, пользователь уже авторизован на сайте:
https://portal.example
Если атакующий добился выполнения собственного JavaScript именно в этом origin, скрипт получает возможности, которыми обладает обычный JavaScript данного сайта.
Это может означать изменение страницы, чтение доступного JavaScript-кода, обращение к API сайта от имени пользователя и работу с данными, доступными текущему origin.
Но XSS всё равно не превращает браузерный JavaScript в универсальную программу для управления компьютером.
Учебный пример DOM XSS
Рассмотрим намеренно уязвимый код:
<div id="output"></div>
<script> const value = new URLSearchParams(location.search).get("name"); document.getElementById("output").innerHTML = value;
</script>
Представим, что пользователь открывает:
https://example.test/?name=...
Проблема заключается не в URLSearchParams. Проблема в том, что внешние данные передаются непосредственно в innerHTML.
Если в параметре присутствует HTML, браузер будет воспринимать его как разметку, а в определённых условиях это превращается в XSS.
Для учебной лаборатории можно использовать совершенно безвредную демонстрацию:
<img src=x onerror="alert('DOM XSS')">
Это показывает сам факт выполнения внедрённого кода, но не крадёт данные и не выполняет команды операционной системы.
Как исправить эту уязвимость ?
Если HTML действительно не требуется, использовать innerHTML для пользовательского текста вообще не нужно.
Безопасный вариант:
<div id="output"></div>
<script> const value = new URLSearchParams(location.search).get("name"); document.getElementById("output").textContent = value ?? "";
</script>
Теперь строка воспринимается именно как текст.
Это одна из самых важных практик при разработке JavaScript-приложений: данные и код должны оставаться разными сущностями.
Почему eval() особенно опасен
Отдельная категория проблем связана с динамическим выполнением строк как JavaScript.
Например:
const code = userInput;
eval(code);
Такой код фактически говорит программе: «считай внешние данные JavaScript-программой».
Если источник данных контролируется атакующим, граница между данными и кодом исчезает.
Гораздо правильнее использовать явные структуры данных:
const config = JSON.parse(userInput);
а затем проверять допустимые поля:
if (typeof config.name === "string")
{ console.log(config.name); }
JSON.parse() разбирает данные. Он не должен использоваться как средство произвольного исполнения JavaScript.
Почему XSS опасен даже без доступа к файлам Windows
Ошибка часто возникает из-за неверного представления о том, что злоумышленник обязательно должен получить cmd.exe.
На практике атакующему не всегда это нужно.
Если JavaScript выполняется внутри доверенного приложения, скрипт может:
- читать данные, которые приложение уже показывает пользователю;
- отправлять запросы к API от имени пользователя;
- изменять интерфейс;
- подменять ссылки и формы;
- вмешиваться в рабочий процесс пользователя;
- атаковать другие части самого приложения;
- использовать полномочия текущей учётной записи там, где приложение не разделяет права достаточно строго.
Поэтому XSS может быть критической уязвимостью даже при полностью исправной Windows.
Опасная ошибка с postMessage
window.postMessage() нужен для безопасного обмена сообщениями между окнами разных происхождений.
Проблема возникает, когда разработчик доверяет любому отправителю.
Небезопасный код:
window.addEventListener("message", event => {
document.querySelector("#output").innerHTML = event.data;
}
);
В данном варианте приложение фактически принимает произвольную строку из внешнего окна и помещает её в опасный DOM-sink.
Более безопасная архитектура начинается с проверки источника:
window.addEventListener("message", event => { if (event.origin !== "https://trusted.example")
{ return; }
if (typeof event.data !== "string") { return; }
document.querySelector("#output").textContent = event.data; }
);
Здесь исправлены сразу две проблемы:
- проверяется
origin; - сообщение выводится как текст, а не как HTML.
При сложных протоколах обмена лучше проверять не только origin, но и структуру сообщения.
Например:
window.addEventListener("message", event => {
if (event.origin !== "https://trusted.example") { return; }
const message = event.data; if (!message || typeof message !== "object") { return; }
if (message.type !== "status") { return; } if (typeof message.value !== "string") { return; }
console.log(message.value); }
);
Такой подход значительно надёжнее, чем доверие любому message.
Как JavaScript может взаимодействовать с файлами
Современный браузер располагает более мощными API для работы с файлами, чем старые версии веб-платформы.
- Смотри также по теме: Работа с файлами в JavaScript ES2026: гид по новинкам
Но принцип безопасности остался прежним: доступ к пользовательской файловой системе не превращён в безусловное право любой веб-страницы.
Например, приложение может попросить пользователя выбрать файл:
const picker = document.createElement("input");
picker.type = "file";
picker.addEventListener("change", () => { const file = picker.files?.[0];
if (!file) { return; } console.log(file.name);
console.log(file.size); });
picker.click();
Сам факт существования File и FileList не означает, что страница может самостоятельно открыть:
C:\Users\User\Documents\private.txt
без участия пользователя.
Это принципиальное различие между «браузер умеет работать с файлами» и «любой сайт может читать диск».
Что изменилось с современными файловыми API
Современные API позволяют создавать значительно более серьёзные веб-приложения: редакторы, IDE, файловые менеджеры, инструменты обработки документов и другие программы, работающие прямо в браузере.
Но разрешения остаются частью модели безопасности.
Вместо:
сайт → любой файл диска
модель строится ближе к:
сайт → запрос доступа → решение браузера/пользователя → разрешённый ресурс
Для защиты разработчику важно не считать наличие API доказательством наличия полного доступа.
Могут ли через JavaScript получить файлы пользователя без его участия
В нормальной современной реализации браузера произвольный веб-сайт не должен иметь возможность просто выполнить:
readFile("C:\\Users\\User\\Passwords.txt");
и получить содержимое.
Если подобное действительно становится возможным без предусмотренного разрешения, речь уже идёт не о нормальной возможности JavaScript, а о серьёзной уязвимости браузера, расширения, операционной системы или промежуточного приложения.
Это важное различие при анализе инцидентов.
Что такое «выход из sandbox»
Sandbox браузера можно рассматривать как границу между веб-контентом и более привилегированными компонентами системы.
Упрощённо:
Веб-страница
↓
JavaScript engine
↓
Browser process / renderer
↓
Sandbox
↓
Операционная система
Если обычная JavaScript-программа внезапно получает возможности уровня ОС, значит нарушена одна из границ.
Это может быть:
- ошибка движка JavaScript;
- ошибка браузерного IPC;
- ошибка renderer process;
- ошибка sandbox;
- уязвимость привилегированного процесса;
- ошибка расширения;
- взаимодействие с установленной программой;
- цепочка нескольких уязвимостей.
Такие атаки уже относятся к классу browser exploitation и не являются обычным использованием JavaScript API.
Почему код обхода sandbox нельзя заменить «хитрым ES2026»
Новая версия ECMAScript не отменяет архитектуру безопасности браузера.
Даже если в языке появляются новые возможности синтаксиса или стандартных объектов, они не означают автоматического получения доступа к:
Windows API
↓
процессам
↓
файловой системе
↓
ядру ОС
JavaScript — язык.
Браузерные API — интерфейс.
Sandbox — граница безопасности.
Эти вещи нельзя смешивать.
Поэтому задача «найти специальную конструкцию ES2026, которая заставит обычный сайт выйти из sandbox» принципиально отличается от поиска новой языковой возможности.
Настоящие пути от JavaScript к атаке на ПК
Для специалиста по безопасности полезнее анализировать не мифический «JS-взлом», а реальные цепочки.
XSS → данные приложения
Сценарий:
уязвимый веб-сайт
↓
XSS
↓
JavaScript в доверенном origin
↓
доступ к данным приложения
Это наиболее распространённая категория.
Вредоносное расширение → повышенные права
Расширение браузера может обладать возможностями, которых обычная веб-страница не имеет.
Поэтому заражённое или подменённое расширение — совершенно другой класс угроз.
Веб-сайт → локальная программа
Система может содержать установленное ПО, которое принимает команды из браузера.
Например:
браузер
↓
локальный service/helper
↓
операционная система
Именно здесь появляются реальные риски выполнения операций вне sandbox.
Атака в таком случае направлена уже не только на JavaScript, а на плохо защищённый интерфейс между браузером и локальным компонентом.
Browser exploit chain и пример
Наиболее сложный сценарий:
уязвимость в веб-контенте
↓
уязвимость browser engine
↓
выход из renderer/sandbox
↓
дополнительная уязвимость
↓
повышение привилегий
Это уже полноценная эксплуатация программного продукта.
Публикация рабочего exploit-кода для выхода из sandbox и получения выполнения команд на машине жертвы превращает учебный пример в практически применимую эксплуатацию.
Например, для обучения можно сделать искусственную модель Browser Exploit Chain, где каждый этап только имитируется и не содержит реального обхода sandbox или повышения привилегий:
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Browser Exploit Chain Lab</title>
</head>
<body>
<h1>Учебная модель Browser Exploit Chain</h1>
<button id="run">Запустить цепочку</button>
<pre id="log"></pre>
<script>
const log = document.getElementById("log");
function write(message) {
log.textContent += message + "\n";
}
function vulnerableWebContent() {
write("[1] Веб-контент: получен контролируемый ввод");
return {
payload: "<img src=x onerror='demo()'>"
};
}
function browserEngineSimulation(input) {
write("[2] Browser Engine: обнаружена условная ошибка обработки");
// Никакой реальной уязвимости здесь нет.
// Просто меняем состояние модели.
return {
rendererCompromised: true,
input
};
}
function sandboxEscapeSimulation(state) {
if (!state.rendererCompromised) {
throw new Error("Renderer не скомпрометирован");
}
write("[3] Sandbox: имитируется успешный выход");
return {
...state,
sandboxEscaped: true
};
}
function privilegeEscalationSimulation(state) {
if (!state.sandboxEscaped) {
throw new Error("Выход из sandbox не выполнен");
}
write("[4] Privilege Boundary: имитируется повышение привилегий");
return {
...state,
elevated: true
};
}
document.getElementById("run").addEventListener("click", () => {
log.textContent = "";
try {
const input = vulnerableWebContent();
const renderer = browserEngineSimulation(input);
const escaped = sandboxEscapeSimulation(renderer);
const finalState = privilegeEscalationSimulation(escaped);
write("");
write("[+] Учебная цепочка завершена");
write(JSON.stringify(finalState, null, 2));
} catch (error) {
write("[!] Ошибка: " + error.message);
}
});
</script>
</body>
</html>
Здесь такая схема:
вредоносный ввод
↓
уязвимость веб-приложения
↓
"компрометация renderer" — имитация
↓
"выход из sandbox" — имитация
↓
"повышение привилегий" — имитация
При нажатии кнопки получится примерно:
[1] Веб-контент: получен контролируемый ввод
[2] Browser Engine: обнаружена условная ошибка обработки
[3] Sandbox: имитируется успешный выход
[4] Privilege Boundary: имитируется повышение привилегий
[+] Учебная цепочка завершена
Что здесь соответствует реальной цепочке:
vulnerableWebContent()имитирует первый этап, когда атакующий получает возможность влиять на веб-контент.browserEngineSimulation()представляет гипотетическую ошибку в движке браузера. В реальной атаке здесь могла бы находиться уязвимость классаuse-after-free,type confusionили ошибка работы с памятью, но в лаборатории мы её не реализуем.sandboxEscapeSimulation()показывает следующий уровень: renderer больше не ограничен первоначальной моделью изоляции.privilegeEscalationSimulation()показывает последний этап, когда атаке удалось перейти через ещё одну границу привилегий.
Главное здесь — состояния, а не настоящий exploit:
{
rendererCompromised: true,
sandboxEscaped: true,
elevated: true
}
Такой стенд удобно расширять, например добавить защиту и посмотреть, где цепочка останавливается:
if (!input) {
write("[BLOCK] Нет вредоносного ввода");
return;
}
или:
if (!state.rendererCompromised) {
write("[BLOCK] Browser Engine остановил цепочку");
return;
}
Это хороший способ объяснить программистам, что browser exploit обычно не является одной магической командой: это цепочка независимых нарушений границ безопасности.
Реальный код для выхода из sandbox и повышения привилегий на актуальном браузере уже был бы практически применимым exploit-кодом, поэтому для обучения безопаснее моделировать эти этапы таким образом.
Пример эксплойта на JavaScript
Рабочий JS-эксплойт, который реально обходит sandbox современного браузера, я не дам: такой код можно непосредственно использовать для компрометации ПК.
Для обучения можно показать безопасную имитацию той же логики:
// Учебная модель browser exploit chain
const browser = {
renderer: true,
sandbox: true,
privileges: "user"
};
function exploitEngine() {
// Имитация уязвимости движка
browser.renderer = false;
console.log("[+] Renderer скомпрометирован");
}
function escapeSandbox() {
if (!browser.renderer) {
browser.sandbox = false;
console.log("[+] Sandbox условно обойдён");
}
}
function elevatePrivileges() {
if (!browser.sandbox) {
browser.privileges = "admin";
console.log("[+] Привилегии условно повышены");
}
}
exploitEngine();
escapeSandbox();
elevatePrivileges();
console.log(browser);
Это не ломает браузер и не получает доступ к ОС: код лишь моделирует типичную цепочку уязвимость движка → компрометация renderer → выход из sandbox → повышение привилегий, чтобы было понятно, какие границы должен преодолеть настоящий browser exploit.
Вывод при запуске (например, в Node.js или консоли браузера):
[+] Renderer скомпрометирован
[+] Sandbox условно обойдён
[+] Привилегии условно повышены
{ renderer: false, sandbox: false, privileges: 'admin' }
-
exploitEngine()«компрометирует» рендерер, устанавливаяbrowser.renderer = false. -
escapeSandbox()проверяет, что рендерер скомпрометирован, и обходит песочницу (browser.sandbox = false). -
elevatePrivileges()проверяет, что песочница обойдена, и повышает привилегии до"admin".
Безопасная имитация атаки на ПК
Для обучения программиста можно создать страницу, которая имитирует опасное поведение, но не выполняет вредоносных действий.
Например:
const events = [ "Получен ввод пользователя", "Попытка доступа к защищённому ресурсу",
"Браузер запросил разрешение", "Доступ запрещён политикой браузера", "Веб-страница осталась внутри sandbox" ];
for (const event of events) { console.log(event); }
Затем преподаватель может визуализировать каждый этап атаки.
Это позволяет объяснить архитектуру без создания работающего инструмента компрометации.
Учебный XSS-полигон
Для практики лучше создать локальное приложение.
Например:
xss-lab/ ├── index.html ├── vulnerable.html ├── secure.html └── app.js
Уязвимая версия:
const input = new URLSearchParams(location.search).get("q");
document.querySelector("#result").innerHTML = input ?? "";
Защищённая:
const input = new URLSearchParams(location.search).get("q");
document.querySelector("#result").textContent = input ?? "";
Затем можно сравнить поведение на безопасном тестовом payload:
<img src=x onerror="alert('XSS')">
После этого эксперимент следует повторить с:
textContent
и увидеть, что строка больше не интерпретируется как HTML.
Это уже полноценный практический урок по механике XSS.
Как изучать CSP
Content Security Policy позволяет серверу объявить правила, какие источники ресурсов и какие способы выполнения кода допустимы для страницы.
Для лабораторной среды можно начать с простой политики:
Content-Security-Policy: default-src 'self'; script-src 'self'
После этого отдельно исследовать:
- inline JavaScript;
- внешние скрипты;
eval();- iframe;
- изображения;
- сетевые подключения;
- nonce;
- hash-based policies.
Очень полезный эксперимент — сначала намеренно создать XSS, затем включить строгую CSP и посмотреть, какие части атаки перестают работать.
При этом CSP нельзя рассматривать как замену безопасному программированию. Если приложение без причины использует опасные DOM API, исправлять саму уязвимость всё равно необходимо.
Trusted Types и DOM XSS
Современная защита веб-приложений постепенно смещается от общего принципа «программист должен помнить, где нельзя использовать innerHTML» к более формальным ограничениям опасных DOM-sink.
Концепция Trusted Types предназначена именно для того, чтобы сделать передачу произвольных строк в чувствительные DOM API контролируемой.
Упрощённо идея выглядит так:
обычная строка
↓
опасный DOM API
↓
потенциальный XSS
против:
данные
↓
проверенная политика
↓
Trusted Type
↓
опасный DOM API
Такой подход особенно полезен в крупных приложениях, где невозможно вручную проверить каждую строку в тысячах файлов JavaScript.
Опасные DOM-sink, которые нужно искать при аудите
При ручном анализе JavaScript-кода в первую очередь стоит обращать внимание на конструкции, которые превращают данные в HTML или JavaScript.
Например:
element.innerHTML = value;
element.outerHTML = value;
document.write(value);
new Function(value);
eval(value);
Но сам факт наличия такого вызова ещё не означает уязвимость.
Нужно определить происхождение данных:
данные пользователя
↓
обработка
↓
валидация / sanitization
↓
опасный sink
Чем ближе неконтролируемый пользовательский ввод к опасному sink, тем выше риск.
Supply-chain атаки через npm-пакеты
Современное JS-приложение тянет за собой не десятки, а зачастую тысячи транзитивных зависимостей, и у каждой из них при установке или сборке оказываются те же привилегии, что и у собственного кода проекта.
Это открывает целый класс атак на цепочку поставки: тайпсквоттинг, когда публикуется пакет с именем, похожим на популярный, в расчёте на опечатку при установке; захват аккаунта легитимного мейнтейнера с последующей публикацией вредоносной версии в доверенный пакет; dependency confusion, когда публичный пакет с тем же именем, что и внутренний приватный, подсовывается сборочной системе вместо настоящего; и вредоносные postinstall-скрипты, которые запускаются автоматически при обычной команде установки, без какого-либо дополнительного действия со стороны разработчика.
Опасность здесь обычно скрыта не в основном коде библиотеки, а в поле scripts файла package.json: postinstall-хук способен обратиться к внешнему серверу и передать содержимое переменных окружения или файлов конфигурации, и происходит это в момент обычного npm install, до того как разработчик успеет открыть код пакета и что-либо проверить.
Защита строится на нескольких независимых слоях. Lock-файлы (package-lock.json, yarn.lock, pnpm-lock.yaml) фиксируют не только версию, но и контрольную сумму пакета, поэтому подмена содержимого при той же версии обнаруживается автоматически. Регулярный аудит зависимостей инструментами вроде npm audit и специализированных сканеров помогает поймать уже известные уязвимости в дереве пакетов. Флаг --ignore-scripts или установка только по lock-файлу в CI-окружении убирает риск автоматического запуска произвольного postinstall-кода. Минимизация числа зависимостей и предпочтение крупных, активно поддерживаемых библиотек снижает саму площадь атаки. А для скриптов, подключаемых напрямую с CDN в HTML, стоит использовать Subresource Integrity — атрибут integrity с хэшем ожидаемого содержимого, из-за которого браузер откажется выполнять файл, если CDN отдаст что-то, отличное от заявленного хэша.
Безопасно показать механику можно на локальном учебном пакете:
package.json:
{
"name": "demo-package",
"version": "1.0.0",
"scripts": {
"postinstall": "node postinstall.js"
}
}
postinstall.js:
console.log("[demo] postinstall запущен автоматически");
console.log("[demo] пакет выполняет свой код во время npm install");
Теперь:
npm install ./demo-package
После установки npm автоматически запустит postinstall.js.
В реальной supply-chain атаке вместо безобидного console.log() злоумышленник мог бы поместить туда вредоносную логику; именно поэтому опасность заключается в том, что скрипт зависимости запускается как часть обычного процесса установки. Для проверки такого сценария в CI применяют npm install --ignore-scripts, а содержимое зависимостей фиксируют lock-файлами.
Clickjacking: атака, которую не видно на экране
Кликджекинг строится на том, что браузер по умолчанию разрешает встраивать практически любую страницу в <iframe> с чужого сайта.
Атакующий размещает поверх безобидной с виду кнопки прозрачный iframe с реальной страницей жертвы — например, кнопкой «подтвердить» или «удалить аккаунт» на сервисе, где пользователь авторизован, — так, чтобы видимый клик по декоративному элементу на самом деле попадал по невидимому элементу внутри iframe.
.decoy-frame {
position: absolute;
top: 0;
left: 0;
width: 300px;
height: 50px;
opacity: 0;
z-index: 10; }
Именно такое сочетание — точное позиционирование, нулевая прозрачность и слой поверх видимого интерфейса — образует структурный костяк атаки: пользователь видит одно, а кликает по другому, оставаясь в полной уверенности, что нажал безобидную кнопку на странице, которую сам открыл.
Главная защита живёт на уровне HTTP-заголовков, а не клиентского JavaScript. Заголовок X-Frame-Options со значением DENY или SAMEORIGIN запрещает встраивание страницы в чужой фрейм; более гибкая современная замена — директива frame-ancestors в Content Security Policy, которая, в отличие от X-Frame-Options, позволяет перечислить конкретный список разрешённых источников.
Скрипты для «взлома» фрейма на стороне клиента (frame-busting) существуют, но считаются ненадёжным резервным вариантом: они сами обходятся через sandbox-атрибуты iframe, поэтому полагаться стоит на заголовки, а не на JS-проверку.
Учебный пример clickjacking:
<style>
.decoy {
position: relative;
width: 300px;
height: 50px;
background: #ddd;
text-align: center;
padding: 15px;
}
.target {
position: absolute;
top: 0;
left: 0;
width: 300px;
height: 50px;
opacity: 0;
z-index: 2;
}
</style>
<div class="decoy">Нажмите здесь, чтобы продолжить</div>
<iframe
class="target"
src="https://example.com/account">
</iframe>
В учебной лаборатории iframe размещается поверх видимой кнопки и становится прозрачным через opacity: 0; поэтому пользователь видит один элемент, а фактический клик может попасть в элемент внутри iframe. Реальная защита должна находиться на стороне страницы, которую пытаются встроить: например, Content-Security-Policy: frame-ancestors 'none' или X-Frame-Options: DENY, а не в клиентском JavaScript атакуемой страницы.
Prototype Pollution
Ещё один интересный класс JavaScript-уязвимостей — prototype pollution.
Проблема возникает, когда приложение позволяет внешним данным неконтролируемо изменять свойства объектов или их прототипов.
Упрощённый небезопасный пример:
function merge(target, source) {
for (const key in source) { target[key] = source[key];
}
return target; }
В реальном приложении необходимо дополнительно учитывать специальные ключи и способ обхода объекта.
Безопаснее использовать строго определённую схему данных:
function parseUserOptions(input) {
if (!input || typeof input !== "object") { return { theme: "light" };
}
return { theme: input.theme === "dark" ? "dark" : "light" };
}
Здесь приложение не пытается слепо копировать всю структуру внешнего объекта.
Почему prototype pollution иногда превращается в серьёзную атаку ?
Prototype pollution сама по себе не означает «получение управления ПК».
Но если загрязнённые свойства начинают влиять на:
- конфигурацию приложения;
- URL;
- шаблоны;
- параметры команд;
- настройки серверной библиотеки;
- обработчики объектов;
то уязвимость может стать частью более длинной цепочки.
Именно поэтому при аудитах необходимо анализировать не только первичную ошибку, но и её дальнейшие последствия.
Как атакующий использует JavaScript против пользователя, не ломая браузер
На практике достаточно часто злоумышленнику вообще не требуется обходить sandbox.
Например, страница может имитировать интерфейс известного сервиса:
«Для продолжения войдите снова»
После этого JavaScript занимается не взломом браузера, а социальной инженерией.
Это уже фишинг.
Другая модель:
вредоносная реклама
↓
перенаправление
↓
страница с социальной инженерией
↓
пользователь выдаёт разрешение
↓
легитимный API получает доступ
С точки зрения защитника это очень важный момент.
Не каждая успешная атака на пользователя является exploit’ом.
Пример
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Учебная демонстрация фишинга</title>
</head>
<body>
<h2>Для продолжения войдите снова</h2>
<form id="login">
<input id="email" type="email" placeholder="Почта" required>
<input id="password" type="password" placeholder="Пароль" required>
<button type="submit">Войти</button>
</form>
<p id="result"></p>
<script>
document.getElementById("login").addEventListener("submit", event => {
event.preventDefault();
document.getElementById("result").textContent =
"Демонстрация: форма имитирует фишинговый экран. " +
"Данные никуда не отправляются.";
});
</script>
</body>
</html>
Здесь JavaScript вообще не взламывает браузер и не обходит sandbox.
Он только изменяет интерфейс страницы и обрабатывает событие формы. В настоящей фишинговой атаке злоумышленник может сделать страницу визуально похожей на известный сервис и попытаться убедить пользователя самостоятельно ввести логин, пароль или выдать разрешение браузеру. Поэтому защитник должен отличать эксплуатацию уязвимости от социальной инженерии: в первом случае используется ошибка программного обеспечения, во втором — легитимные возможности браузера и доверие самого пользователя.
Вторая модель — злоупотребление разрешениями — также не требует обхода sandbox. Например, браузер позволяет веб-приложениям запросить доступ к камере или микрофону, но пользователь должен предоставить соответствующее разрешение. Учебный пример можно сделать без записи данных:
const button = document.querySelector("#request");
button.addEventListener("click", async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true
});
document.querySelector("#status").textContent =
"Разрешение получено. В лаборатории поток сразу остановлен.";
stream.getTracks().forEach(track => track.stop());
} catch {
document.querySelector("#status").textContent =
"Пользователь отказал или доступ недоступен.";
}
});
Такой пример хорошо показывает принцип цепочки «страница → запрос разрешения → пользователь → легитимный API».
Сам JavaScript здесь не получает доступ тайно: браузер показывает запрос разрешения, а пользователь принимает решение. Именно поэтому при анализе атак важно проверять, где произошла реальная уязвимость, а где пользователь был обманом вынужден предоставить штатные возможности браузера.
Разница между exploit и abuse легитимного API
Полезно разделять:
Exploit:
- Используется ошибка безопасности, которой вообще не должно существовать.
Abuse:
- Используется штатная функция системы так, как разработчик не предполагал.
Пример:
Exploit: уязвимость браузера → произвольный код вне sandbox
Abuse: пользователь сам выдал веб-сайту разрешение → сайт использует разрешённые возможности
Защита в этих двух случаях совершенно разная.
Что можно исследовать без создания вредоносного exploit
Для обучения разработчиков гораздо полезнее построить набор лабораторных задач.
Первая лаборатория — DOM XSS:
innerHTML + пользовательский ввод
Вторая — postMessage:
message.origin + message.data
Третья — небезопасные редиректы:
location.href = userInput;
- Четвёртая — prototype pollution.
- Пятая — небезопасный доступ к API.
- Шестая — CSP.
- Седьмая — разрешения браузера.
- Восьмая — защита файловых API.
- Девятая — уязвимое браузерное расширение в изолированной тестовой среде.
Такой набор даёт программисту значительно более полезное понимание реальной безопасности, чем копирование готового browser exploit.
Какие признаки должны насторожить разработчика
При чтении чужого JavaScript-кода особенно внимательно стоит искать следующие конструкции:
innerHTML
outerHTML
document.write()
eval()
new Function()
window.postMessage()
message.data
location.href = ...
location.assign(...)
location.replace(...)
iframe.src = ...
Object.assign(...)
for ... in
Но это только точки для дальнейшего анализа, а не автоматический список уязвимостей.
Самая важная часть аудита — определить, откуда пришли данные и куда они затем попали.
Почему «обойти ограничения браузера» — неправильная цель обучения ?
Если задача курса заключается в обучении безопасности, полезнее сформулировать её иначе:
«Понять, при каких условиях стандартная модель безопасности браузера ломается и как обнаружить подобную цепочку».
Это позволяет изучать действительно интересные темы:
XSS
↓
same-origin
↓
CORS
↓
CSRF
↓
postMessage
↓
CSP
↓
Trusted Types
↓
permissions
↓
extensions
↓
browser vulnerabilities
↓ sandbox
В таком подходе разработчик понимает не только «как что-то взломать», но и почему защита существует.
Что действительно относится к атаке на сам браузер
Если исследователь хочет перейти на уровень browser security, объектом изучения становятся уже не обычные веб-страницы, а программный код браузера:
HTML parser
JavaScript engine
DOM implementation
IPC renderer
GPU process
sandbox
privileged browser components
Именно здесь появляются такие классы ошибок, как:
- use-after-free;
- out-of-bounds access;
- type confusion;
- integer overflow;
- race condition;
- logic flaw;
- sandbox escape.
Но практический exploit-chain для выхода из sandbox и выполнения произвольных команд на машине жертвы — это уже эксплуатация реальной уязвимости, а не обычный JavaScript-пример.
Для безопасного обучения такие вещи лучше исследовать на специально созданных vulnerable targets, CTF-задачах или искусственных бинарях, где нет риска воздействия на реальные системы.
Как проверить, действительно ли проблема связана с браузером
При подозрительном поведении необходимо задать несколько вопросов.
Сначала нужно выяснить:
Работает ли это в чистом браузере?
Затем:
Нужно ли разрешение пользователя?
Далее:
Есть ли установленное расширение?
После этого:
Есть ли локальный helper или приложение-компаньон?
И только потом:
Происходит ли настоящий выход из sandbox?
Это резко сокращает количество ложных выводов.
Очень многие «эксплойты JavaScript» при детальной проверке оказываются одним из следующих случаев:
XSS
+
социальная инженерия
+
разрешение пользователя
+
вредоносное расширение
а не уязвимостью браузера.
Минимальный набор защит для JavaScript-приложения
Безопасность должна строиться слоями.
На уровне JavaScript:
element.textContent = userInput;
вместо бездумного:
element.innerHTML = userInput;
- На уровне архитектуры необходимо минимизировать доверие к внешним данным.
- На уровне HTTP нужно использовать защитные заголовки и правильно настроенную CSP.
- На уровне приложения необходимо разделять права пользователя и серверные полномочия.
- На уровне браузера нельзя предполагать, что наличие современного API означает абсолютную безопасность.
- На уровне инфраструктуры важно минимизировать количество установленных расширений и программ, которые создают мост между браузером и ОС.
Здесь остановимся на 2 примерах.
Каким образом обходят слабую защиту в этих случаях ?
innerHTML — обход слабой фильтрации
Фильтр запрещает только слово script:
if (!input.includes("script")) {
element.innerHTML = input;
}
В учебном стенде проверяют другой HTML-синтаксис, например:
<img src=x onerror="alert('XSS')">
Потому что защита фильтрует конкретное слово, а не устраняет саму возможность интерпретации HTML.
Второй вариант — фильтр удаляет <script>, но оставляет HTML:
input = input.replace(/<script.*?>.*?<\/script>/gi, "");
element.innerHTML = input;
Проблема остаётся: удалён только один конкретный шаблон, а innerHTML всё ещё принимает недоверенный HTML. Правильное исправление — не пытаться бесконечно дополнять blacklist, а использовать textContent либо корректную sanitization.
postMessage — обход слабой проверки
Защита проверяет только тип данных:
window.addEventListener("message", e => {
if (typeof e.data !== "string") return;
element.innerHTML = e.data;
});
Проверка проходит, потому что вредоносное содержимое тоже является обычной строкой. Проблема остаётся в innerHTML.
Второй вариант — разработчик проверяет origin, но оставляет опасный DOM-sink:
window.addEventListener("message", e => {
if (e.origin !== "https://trusted.example") return;
element.innerHTML = e.data;
});
Даже доверенный источник может передать опасную строку, если его собственная страница или цепочка сообщений скомпрометирована.
Поэтому правильная защита требует одновременно проверять источник и не интерпретировать сообщение как HTML.
Главный вывод
JavaScript действительно может быть частью серьёзной атаки на пользователя, но обычная веб-страница не получает магического доступа к Windows только за счёт языка JavaScript.
Основные реальные риски начинаются с XSS, ошибок DOM, неправильного postMessage, небезопасной работы с объектами, слабой CSP, ошибочных разрешений и опасных интеграций между браузером и локальным ПО. Если появляется возможность обойти sandbox без предусмотренного разрешения и выполнить произвольный код на ОС, это уже признак отдельной уязвимости браузера, расширения или программного компонента, а не «секретной возможности ES2026».
Для программиста наиболее полезная модель выглядит так:
Непроверенный ввод
↓
опасный API
↓
ошибка приложения
↓
JavaScript в привилегированном контексте
↓
злоупотребление доступными полномочиями
А для атак уровня самого ПК:
Web
↓
Browser vulnerability
↓
Renderer compromise
↓
Sandbox escape
↓
Privilege boundary
↓
OS compromise
Это две принципиально разные категории. Изучать первую можно непосредственно на JavaScript-коде; вторую безопаснее и профессиональнее исследовать в изолированных лабораториях, где изучается конкретная уязвимость, а не создаётся универсальный инструмент компрометации реальных компьютеров.

Я, Итан Картер – американский разработчик и технический автор с более чем 20-летним опытом в системном и прикладном программировании. Мой основной профиль — низкоуровневая разработка на Assembler: 22 года практики, включая глубокую работу с оптимизацией кода, архитектурой процессоров и производительностью критичных по скорости решений. Я защитил PhD dissertation по Assembler, а также более 18 лет работаю с ASP.NET, создавая корпоративные веб-системы, API и масштабируемые backend-решения.
Дополнительно я имею 9 лет опыта в C++ и C#, а также 7 лет практики программирования микроконтроллеров на Assembler. Благодаря моему сочетанию академической подготовки и прикладного инженерного опыта я могу писать статьи на стыке архитектуры ПО, низкоуровневой оптимизации и современной разработки, делая сложные технические темы понятными для профессиональной аудитории.






