惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

有赞技术团队
有赞技术团队
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
P
Palo Alto Networks Blog
C
Cisco Blogs
The Hacker News
The Hacker News
T
Threatpost
S
Schneier on Security
K
Kaspersky official blog
Spread Privacy
Spread Privacy
博客园_首页
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
NISL@THU
NISL@THU
量子位
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Google DeepMind News
Google DeepMind News
Security Latest
Security Latest
博客园 - 司徒正美
云风的 BLOG
云风的 BLOG
博客园 - 叶小钗
H
Hackread – Cybersecurity News, Data Breaches, AI and More
N
News and Events Feed by Topic
爱范儿
爱范儿
P
Proofpoint News Feed
C
CERT Recently Published Vulnerability Notes
Project Zero
Project Zero
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Cisco Talos Blog
Cisco Talos Blog
GbyAI
GbyAI
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Apple Machine Learning Research
Apple Machine Learning Research
T
Tenable Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Vulnerabilities – Threatpost
Forbes - Security
Forbes - Security
博客园 - 三生石上(FineUI控件)
C
Cyber Attacks, Cyber Crime and Cyber Security
N
News and Events Feed by Topic
V
V2EX
Webroot Blog
Webroot Blog
The Register - Security
The Register - Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Blog — PlanetScale
Blog — PlanetScale
M
MIT News - Artificial intelligence
Scott Helme
Scott Helme
Simon Willison's Weblog
Simon Willison's Weblog
L
LangChain Blog
W
WeLiveSecurity
Cloudbric
Cloudbric

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
8 песочниц в браузере без Docker: как мы изолировали выполнение кода на клиенте
Борис Карабут · 2026-06-26 · via Все публикации подряд на Хабре

Средний

11 мин

34

Мы разрабатывали сайт с инструментами для разработчиков — форматтеры, конвертеры, генераторы, всякое полезное, что нужно под рукой каждый день. Довольно быстро пришло понимание: без интерактивных песочниц проект будет неполным. Пользователю нужно не просто отформатировать JSON или сгенерировать UUID, но и иметь возможность быстро проверить JavaScript-сниппет, прогнать SQL-запрос, поэкспериментировать с Python. Причём хотелось сделать это по-настоящему удобно: бесплатно, без рекламы и без регистрации. Просто открываешь и пользуешься. Никаких "доступно только в Pro-версии" или "вы исчерпали лимит компиляций".

Но как только мы начали проектировать архитектуру песочниц, стало понятно, что желание красивое, а реальность — ящик Пандоры. Дать пользователю возможность выполнять произвольный код прямо в браузере — значит открыть дверь для целого букета проблем: бесконечные циклы, XSS-векторы, попытки доступа к DOM, утечки памяти, вредоносные запросы. Каждый пункт по-своему опасен, и каждый нужно как-то закрывать.

При этом мы принципиально не хотели использовать серверные рантаймы. Никаких Docker-контейнеров, никаких выделенных виртуалок, никакого проксирования кода на бэкенд. Всё должно работать на клиенте — быстро, приватно и без затрат на инфраструктуру.

Это означало, что нам нужна система изоляции, которая живёт полностью в браузере. Причём для каждой среды — JavaScript, Python, SQL, HTML/CSS, Markdown, Bash, Regex, Cron — требуется свой подход. Где-то хватает Web Worker, где-то нужен WebAssembly с кастомной загрузкой, а где-то приходится писать эмулятор с нуля.

В итоге мы построили восемь песочниц с разными методами изоляции. Дальше — подробный разбор каждой: как устроена, какие угрозы закрывает и с какими граблями мы столкнулись.

JavaScript: Web Worker и ручной таймаут

JavaScript-песочница — самая очевидная и одновременно самая опасная. Дать пользователю выполнить произвольный код в основном потоке — значит разрешить ему дотянуться до DOM, window, куки и всего остального. Поэтому первое и главное решение: код летит в отдельный Web Worker.

У воркера нет доступа к DOM, document и window основного потока. Но остаются две проблемы: как перехватить вывод console.log и как защититься от бесконечного цикла.

Перехват консоли. Вместо того чтобы глушить вывод совсем, мы подменяем console.log, error, warn и info внутри воркера. Каждый вызов сериализуется в строку и складывается в массив logs. Когда код отработал — массив улетает в основной поток через postMessage:

const logs: string[] = [];

const cons = {
  log: (...a: unknown[]) => {
    logs.push(a.map((x) =>
      typeof x === 'object' ? JSON.stringify(x) : String(x)
    ).join(' '));
  },
  error: (...a: unknown[]) => {
    logs.push('[error] ' + a.map((x) =>
      typeof x === 'object' ? JSON.stringify(x) : String(x)
    ).join(' '));
  },
  // ...warn, info аналогично
};

Подменённый console передаётся в пользовательский код как аргумент — через new Function('console', ...). Никаких глобальных переменных не трогаем, код изолирован на уровне замыкания.

Бесконечный цикл. Воркер — это отдельный поток, так что UI не зависнет в любом случае. Но сам воркер может висеть вечно, и нам нужно уметь его прибивать. Решение — setTimeout с таймаутом, который триггерит terminate:

const timer = setTimeout(() => finish({ kind: 'timeout' }), timeoutMs);

Как только таймаут срабатывает — воркер отправляет сообщение { kind: 'timeout' } и сбрасывает флаг finished, чтобы последующие вызовы finish игнорировались. Пользовательский код выполняется через async/await внутри замыкания — если он завис в синхронном бесконечном цикле, то до await дело не дойдёт, но таймаут всё равно сработает и воркер будет уничтожен снаружи вызовом terminate.

Есть нюанс: бесконечный цикл внутри setTimeout или Promise, созданного пользователем, таймаут не перехватит — но сам воркер уже будет terminated к тому моменту.

Лимиты: 200 000 символов на код, таймаут 10 секунд. Воркер создаётся заново на каждый запуск — никакого переиспользования, чтобы исключить утечки состояния.

Python: Pyodide с цепочкой зеркал

Python-песочница построена на Pyodide — CPython, скомпилированном в WebAssembly. И в этом главная проблема: Pyodide — это здоровенный Wasm-файл, и его нужно откуда-то грузить.

Fallback по зеркалам. Мы не стали завязываться на один CDN. Вместо этого определили три источника и пробуем их по очереди:

const INDEX_CANDIDATES = [
  'https://cdn.jsdelivr.net/pyodide/v0.26.1/full/',
  'https://cdn.jsdelivr.net/pyodide/v0.25.0/full/',
  'https://unpkg.com/pyodide@0.26.1/full/',
];

Функция pickWorkingIndexBase делает HEAD-запрос к pyodide.js каждого зеркала и возвращает первое доступное. Только после этого вызывается importScripts и loadPyodide. Это работает в Web Worker — основной поток вообще не участвует в загрузке.

Изоляция. Pyodide из коробки не даёт доступа к файловой системе хоста. Стандартная библиотека доступна — json, re, math и так далее. Но import os не сработает, потому что нет реальной операционной системы под капотом. Никакого pip тоже нет — только то, что уже вкомпилировано в Pyodide.

Перехват вывода. Pyodide предоставляет методы setStdout и setStderr. Мы вешаем на них batched-колбэки, которые собирают вывод в строку. Важный нюанс: Pyodide может слать несколько print в одном батче, без \n между ними. Поэтому мы дописываем перевод строки после неполных чанков:

const appendLine = (buf: string, s: string) => {
  if (!s) return buf;
  
  return buf + (s.endsWith('\n') ? s : `${s}\n`);
};

Таймаут. Реализован так же, как в JS-песочнице: setTimeout на нужное количество миллисекунд плюс запас на тормоза Wasm. При срабатывании отправляется { type: 'run-timeout' }. Плюс есть специальное сообщение { type: 'cancel' }, которое сбрасывает текущий таймер — на случай, если пользователь запустил код заново, не дожидаясь завершения предыдущего.

Лимиты: 100 000 символов на код, таймаут 25 секунд (Python в Wasm заметно медленнее нативного JS).

SQL: SQL.js в Web Worker с ручным locateFile

Для SQL-песочницы мы выбрали SQL.js — SQLite, скомпилированный в WebAssembly. Работает внутри Web Worker, чтобы не нагружать основной поток при парсинге тяжёлых запросов.

Загрузка Wasm без CDN. sql.js по умолчанию пытается загрузить wasm-файл с CDN (sql.js.org). Это не подходит: во-первых, лишняя зависимость от внешнего источника, во-вторых — на sql.js.org нет файла sql-wasm-browser.wasm, который запрашивает бандл (там лежит HTML вместо wasm, и движок падает с ошибкой). Поэтому мы переопределяем locateFile:

function locateSqlWasm(file: string): string {
  const origin = self.location?.origin;
  
  if (!origin || !file.endsWith('.wasm')) {
    return `https://sql.js.org/dist/${file}`;
  }
  
  if (file.includes('sql-wasm-browser')) {
    return `${origin}/wasm/sql-wasm-browser.wasm`;
  }
  
  return `${origin}/wasm/sql-wasm.wasm`;
}

Wasm-файлы раздаются с того же origin, что и страница. Никаких внешних CDN — быстрее и надёжнее.

Инициализация базы. Каждый запуск начинается с инициализации: создаётся новый экземпляр Database, накатывается схема из пользовательского ввода:

db = new S.Database();

if (msg.payload.schemaSql.trim()) {
  db.run(msg.payload.schemaSql);
}

Это позволяет пользователю создать таблицы, вставить тестовые данные и сразу писать запросы к ним.

Выполнение запросов с AbortController. Главная опасность SQL-песочницы — запрос, который вешает базу (например, cross join на больших таблицах или рекурсивный CTE). sql.js выполняет запросы синхронно, и просто setTimeout тут не поможет. Поэтому мы используем AbortController:

const ac = new AbortController();

const timer = setTimeout(() => ac.abort(), timeoutMs);
const results = db.exec(sql);

if (ac.signal.aborted) {
  post({ type: 'error', payload: { message: 'Превышен таймаут...' } });
  
  return;
}

После выполнения проверяем, не был ли сигнал прерван за время работы db.exec. Если был — возвращаем ошибку таймаута.

Вывод результатов. Если запрос возвращает данные (SELECT), мы отдаём их как { kind: 'select', columns, rows }. Если это INSERT/UPDATE/DELETE — возвращаем { kind: 'ok', message }. Если ошибка — { kind: 'error', message }.

Лимиты: 200 000 символов на скрипт (схема + запрос), таймаут 5 секунд.

HTML/CSS: iframe с sandbox и динамический srcdoc

HTML/CSS-песочница стоит особняком. Здесь нет выполнения кода на серверном рантайме — пользователь просто пишет разметку и стили, а мы должны безопасно показать результат.

Изоляция через iframe sandbox. Превью рендерится в <iframe sandbox="allow-scripts">. Атрибут sandbox включает изоляцию, а allow-scripts разрешает выполнение JavaScript внутри превью, но без доступа к cookie и DOM родительской страницы. Даже если пользователь напишет <script>document.cookie</script>, он не получит сессионные данные Halfcoder.

Сборка srcdoc. Пользователь вводит HTML и CSS отдельно. Мы собираем из них полноценный документ:

function buildSrcDoc(html: string, css: string): string {
  const style = css.trim() ? `<style>${css}</style>` : '';
  
  if (/<html[\s>]/i.test(html)) {
    if (/<head[\s>]/i.test(html)) {
      return html.replace(/<\/head>/i, `${style}</head>`);
    }
    return html.replace(/<html[^>]*>/i, (m) => `${m}<head>${style}</head>`);
  }
  
  return `<!DOCTYPE html><html><head><meta charset="utf-8">${style}</head><body>${html}</body></html>`;
}

Готовый документ передаётся в srcDoc атрибут iframe. Обновление происходит с задержкой 500 мс через debounce — чтобы не перерендеривать превью на каждое нажатие клавиши.

Ограничения. Песочница не проверяет CSS-инъекции — теоретически можно написать стиль, который сломает вёрстку внутри iframe, но наружу это не выйдет. JavaScript внутри превью выполняется в отдельном контексте и не может выйти за пределы iframe.

Markdown: DOMPurify с жёстким вайтлистом

Markdown-песочница, на первый взгляд, самая безобидная. Пользователь пишет Markdown, мы рендерим HTML. Опасность — XSS через встроенный HTML или скрипты, замаскированные под Markdown-разметку.

Санитизация через DOMPurify. После рендеринга через marked, получившийся HTML прогоняется через DOMPurify с жёстко заданным списком разрешённых тегов:

function sanitizeMarkdownHtml(raw: string): string {
  return DOMPurify.sanitize(raw, {
    USE_PROFILES: { html: true },
    ALLOWED_TAGS: [
      'p', 'br', 'strong', 'em', 'code', 'pre',
      'h1', 'h2', 'h3', 'h4',
      'ul', 'ol', 'li', 'blockquote', 'a',
      'hr', 'table', 'thead', 'tbody', 'tr', 'th', 'td',
    ],
    ALLOWED_ATTR: ['href', 'title', 'class'],
    ALLOW_DATA_ATTR: false,
  });
}

Никаких скриптов, никаких on*-обработчиков, никаких data-атрибутов. Даже если пользователь вставит <script>alert('xss')</script> в Markdown, DOMPurify вырежет это на этапе санитизации.

Почему не просто экранировать HTML. Мы могли бы вообще запретить HTML в Markdown, но это сломало бы поддержку таблиц, ссылок и других расширенных фич GFM. DOMPurify даёт золотую середину: HTML работает, но опасные элементы вырезаются.

Bash: самописный эмулятор с пайпами

Bash-песочница — возможно, самая необычная часть системы. Мы не стали подключать настоящий shell через Wasm или эмулятор терминала. Вместо этого написали свой парсер команд и эмулятор пайпов с нуля.

Почему самописный. Готовые решения вроде web-shell или jslinux слишком тяжёлые и заточены под полноценную ОС. Нам нужен был учебный терминал, который поддерживает базовые команды и пайпы, но не требует виртуализации.

Парсинг аргументов. Команды парсятся с учётом кавычек и экранирования:

function splitArgs(line: string): string[] {
  const out: string[] = [];
  
  let cur = '';
  let q: '"' | "'" | null = null;
  
  for (let i = 0; i < line.length; i++) {
    const c = line[i];
    
    if (q) {
      if (c === q) { q = null; } else { cur += c; }
      continue;
    }
    if (c === '"' || c === "'") { q = c as '"' | "'"; continue; }
    if (/\s/.test(c)) {
      if (cur.length) { out.push(cur); cur = ''; }
      continue;
    }
    cur += c;
  }
  
  if (cur.length) out.push(cur);
  
  return out;
}

Пайпы. Главная фича эмулятора — поддержка пайпов. Строка разбивается по |, каждый сегмент выполняется отдельно, stdout предыдущего становится stdin следующего:

const segments = raw.split('|').map(s => trim(s));
let stdin = '';

for (let i = 0; i < segments.length; i++) {
  const { out, env: ne } = runSegment(segments[i], stdin, env);
  e = ne;
  
  if (i === segments.length - 1) outs.push(out);
  else stdin = out.endsWith('\n') ? out : `${out}\n`;
}

Поддерживаемые команды. На старте их восемь: echo, export, pwd, wc, grep, head, tail, cat. Каждая реализована как чистая функция:

function cmdEcho(args: string[]): string {
  return args.join(' ');
}

function cmdWc(args: string[], stdin: string): string {
  const text = stdin.replace(/\n$/, '');
  const lines = text.length ? text.split('\n').length : 0;
  const words = text.trim().length ? text.trim().split(/\s+/).length : 0;
  const bytes = new TextEncoder().encode(stdin).length;
  
  if (args[0] === '-l') return String(lines);
  if (args[0] === '-w') return String(words);
  if (args[0] === '-c') return String(bytes);
  
  return `${lines} ${words} ${bytes}`;
}

Переменные окружения. export работает с реальным объектом Env, который передаётся между командами. Поддерживается подстановка переменных через $VAR:

function expandEnv(s: string, env: Env): string {
  return s.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name: string) => env[name] ?? '');
}

Ограничения. Никаких системных вызовов, никакого доступа к файловой системе (кроме виртуальной PWD = '/sandbox'). Незнакомые команды возвращают ошибку с подсказкой. Скрипты с переводом строки выполняются построчно, как в настоящем shell.

Regex: нативный JavaScript с защитой от ReDoS

Regex-песочница — самая простая технически. Мы не стали изобретать велосипед и используем нативный RegExp движок браузера. Но есть один важный нюанс — ReDoS.

Ограничение совпадений. При глобальном поиске мы насильно ограничиваем количество итераций до 5000:

let m: RegExpExecArray | null;
const copy = new RegExp(pattern, flagStr);
while ((m = copy.exec(haystack)) !== null) {
  matches.push({ index: m.index, match: m[0], groups: m.slice(1) });
  if (m[0].length === 0) copy.lastIndex++;
  if (matches.length > 5000) break;  // защита от ReDoS
}

Плюс ручная защита от пустых совпадений — если длина совпадения 0, мы сдвигаем lastIndex на 1, чтобы цикл не завис.

Примеры с ReDoS. В тестовых данных есть специальный пример infinite с паттерном a и строкой из 5000 символов — это позволяет проверить, что песочница не зависает на реальном ReDoS-векторе.

Cron: парсинг и визуализация без сервера

Cron-песочница — это не выполнение кода, а инструмент для работы с cron-выражениями. Пользователь вводит выражение и получает ближайшие запуски с учётом часового пояса.

Парсинг через cron-parser. Используем cron-parser v5 с поддержкой стандартных cron-выражений:

export function parseCronExpression(expression: string, options?: { currentDate?: Date; tz?: string }) {
  return CronExpressionParser.parse(expression.trim(), {
    currentDate: options?.currentDate ?? new Date(),
    ...(options?.tz ? { tz: options.tz } : {}),
  });
}

Человекочитаемое описание. cronstrue генерирует текстовое описание выражения (например, «Каждые 15 минут»). Даты форматируются через Luxon с учётом часового пояса пользователя.

Изоляция. Здесь нет песочницы в классическом смысле — cron-парсер работает синхронно на клиенте и не выполняет пользовательский код. Единственная защита — try/catch на некорректные выражения.

Общие принципы изоляции

Вот что объединяет все восемь песочниц:

  1. Никакой серверной обработки. Весь код выполняется в браузере. Мы не проксируем запросы и не имеем доступа к пользовательским данным.

  2. Web Worker для тяжёлого. JS, Python, SQL — всё, что может нагрузить основной поток или выполняет пользовательский код, вынесено в воркеры.

  3. Таймауты везде. Каждая песочница имеет лимит по времени выполнения. При превышении — принудительный terminate воркера или возврат ошибки.

  4. Пересоздание на каждый запуск. Мы не переиспользуем воркеры между запусками. Каждый Run создаёт новый Worker, выполняет код и уничтожается. Это исключает утечки состояния и атаки через остаточные данные.

  5. Лимиты по объёму. Каждая песочница имеет ограничение на количество символов во входном коде — от 100 000 (Python) до 200 000 (JS, SQL). Это защита от намеренного переполнения памяти.

  6. Валидация до выполнения. Все входные данные проверяются на пустоту и длину до того, как попасть в рантайм. Это отсекает очевидные атаки на этапе фронтенда.

Что мы вынесли из этого опыта

Самописный Bash — это боль. Парсинг пайпов, экранирование спецсимволов, обработка краевых случаев с кавычками — на это ушло непропорционально много времени. Но готовых лёгких альтернатив мы не нашли, а тащить полноценный эмулятор терминала не хотелось.

Pyodide — тяжёлый, но безальтернативный. Загрузка Wasm-файла занимает время, особенно на мобильных. Но без Pyodide мы не смогли бы дать пользователю настоящий Python в браузере. Fallback по зеркалам спасает от проблем с CDN.

SQL.js и AbortController — опасная связка. db.exec выполняется синхронно, и AbortController не может прервать его посередине. Мы можем только проверить флаг после выполнения. Это значит, что запрос, который вешает SQLite на 10 секунд, реально вешает воркер на 10 секунд — таймаут только сигнализирует об этом постфактум.

DOMPurify — мастхев. Без него Markdown-песочница была бы дырой в безопасности. Вайтлист тегов — это не паранойя, а необходимость, когда рендеришь пользовательский HTML.

Web Worker — серебряная пуля для JS. Просто, надёжно, отлично изолирует. Жаль только, что нельзя заставить пользователя не писать бесконечные циклы внутри setTimeout — но это уже не наша проблема.

Обратная связь и полезные ссылки

Описанные песочницы — часть проекта Halfcoder, сайта, где представлены 167+ инструментов для разработчиков, которые целиком работают в браузере. Если хотите посмотреть на них вживую или потестировать — заходите на halfcoder.ru. Наш проект полностью бесплатный и не содержит рекламы, чтобы it коммьюнити могло без лишних трудностей использовать наши инструменты.

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