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

推荐订阅源

Cisco Talos Blog
Cisco Talos Blog
Latest news
Latest news
G
GRAHAM CLULEY
Security Latest
Security Latest
C
Cyber Attacks, Cyber Crime and Cyber Security
Security Archives - TechRepublic
Security Archives - TechRepublic
N
News and Events Feed by Topic
S
Secure Thoughts
Simon Willison's Weblog
Simon Willison's Weblog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Help Net Security
Help Net Security
S
Schneier on Security
Application and Cybersecurity Blog
Application and Cybersecurity Blog
博客园 - Franky
H
Heimdal Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
The Cloudflare Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
人人都是产品经理
人人都是产品经理
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
有赞技术团队
有赞技术团队
博客园 - 【当耐特】
雷峰网
雷峰网
Forbes - Security
Forbes - Security
IT之家
IT之家
The Hacker News
The Hacker News
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
N
News | PayPal Newsroom
AI
AI
博客园 - 三生石上(FineUI控件)
PCI Perspectives
PCI Perspectives
H
Hacker News: Front Page
Apple Machine Learning Research
Apple Machine Learning Research
N
News and Events Feed by Topic
WordPress大学
WordPress大学
P
Palo Alto Networks Blog
S
SegmentFault 最新的问题
Last Week in AI
Last Week in AI
C
CERT Recently Published Vulnerability Notes
Cyberwarzone
Cyberwarzone
阮一峰的网络日志
阮一峰的网络日志
K
Kaspersky official blog
L
LINUX DO - 最新话题
宝玉的分享
宝玉的分享
S
Security @ Cisco Blogs
Know Your Adversary
Know Your Adversary
小众软件
小众软件
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园_首页

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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-25 · via Все публикации подряд на Хабре

Средний

11 мин

0

Когда мы начинали работать над проектом, нам очень хотелось сделать интерактивные песочницы для кода — такие, в которых можно быстро проверить 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; 
}

Плюс ручная защита от пустых совпадений — если длина совпадения 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, где пишу про IT-новости, делюсь полезными инструментами и иногда кидаю мемы.