From d8717d0fbdda5bdc04c795062f1516eaca644088 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 13 Jun 2026 13:33:14 +0300 Subject: [PATCH] =?UTF-8?q?fix(sim-builder):=20=D0=B2=D0=B0=D0=B9=D1=82?= =?UTF-8?q?=D0=BB=D0=B8=D1=81=D1=82=20=D1=86=D0=B2=D0=B5=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B2=20validateSpec=20=E2=80=94=20=D0=B7=D0=B0=D0=BA=D1=80?= =?UTF-8?q?=D1=8B=D1=82=D1=8C=20CSS-=D0=B8=D0=BD=D1=8A=D0=B5=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=B2=20=D1=88=D0=B0=D1=80=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D1=81=D0=BF=D0=B5=D0=BA=D0=B0=D1=85=20(=D1=84?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B5=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B2=D1=8C=D1=8E)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/customSimController.js | 31 +++++++++++++++++-- plans/sim-builder/PLAN.md | 8 ++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/backend/src/controllers/customSimController.js b/backend/src/controllers/customSimController.js index 6f6520b..4f2878d 100644 --- a/backend/src/controllers/customSimController.js +++ b/backend/src/controllers/customSimController.js @@ -43,6 +43,26 @@ function sanitizeText(v, max = MAX_TEXT_LEN) { return s; } +/* Цвет: пропускаем ТОЛЬКО безопасные формы (#hex, rgb()/rgba()/hsl()/hsla(), + имя-слово). Иначе возвращаем undefined — поле выкидывается, движок берёт дефолт. + Цель: строка вида "#fff;background:url(https://evil)" не должна утечь в + style.cssText при рендере шаренной/опубликованной спеки (CSS-инъекция → исходящий GET). */ +function sanitizeColor(v) { + if (v === null || v === undefined) return undefined; + const s = String(v).trim().slice(0, 40); + if (/^#[0-9a-fA-F]{3,8}$/.test(s)) return s; + if (/^(?:rgb|rgba|hsl|hsla)\(\s*[0-9.,%\s/]+\)$/.test(s)) return s; + if (/^[a-zA-Z]{1,30}$/.test(s)) return s; // named color (red, transparent, ...) + return undefined; // небезопасно — выкинуть +} +function applyColorFields(out, src, keys) { + for (const k of keys) { + if (src[k] === undefined) continue; + const c = sanitizeColor(src[k]); + if (c === undefined) delete out[k]; else out[k] = c; + } +} + /* Строка-выражение: число оставляем числом; строку обрезаем по длине, но НЕ парсим/исполняем (это делает безопасный SimExpr на клиенте). Отклоняем только превышение длины. */ @@ -102,9 +122,13 @@ function validateSpec(spec) { if (spec.meta.desc !== undefined) clean.meta.desc = sanitizeText(spec.meta.desc, 1000); } - // viewport — числовые границы пропускаем как есть (числа/строки). + // viewport — числовые границы пропускаем как есть; bg санитизируем (CSS-инъекция). if (spec.viewport && typeof spec.viewport === 'object' && !Array.isArray(spec.viewport)) { - clean.viewport = spec.viewport; + clean.viewport = { ...spec.viewport }; + if (clean.viewport.bg !== undefined) { + const c = sanitizeColor(clean.viewport.bg); + if (c === undefined) delete clean.viewport.bg; else clean.viewport.bg = c; + } } // time — конфиг t-цикла (autoplay/loop/duration/speed). @@ -139,6 +163,9 @@ function validateSpec(spec) { if (o.unit !== undefined) out.unit = sanitizeText(o.unit, 40); if (o.id !== undefined) out.id = sanitizeText(o.id, 60); + // Цвета — вайтлист (иначе CSS-инъекция через style.cssText при рендере). + applyColorFields(out, o, ['color', 'fill', 'fillColor', 'trailColor', 'bg']); + // Строки-выражения: координаты/радиусы/выражения/диапазоны. for (const k of ['x', 'y', 'x1', 'y1', 'x2', 'y2', 'r', 'w', 'h', 'dx', 'dy', 'expr', 'size', 'width', 'precision', 'samples']) { if (o[k] !== undefined) checkExpr(o[k], `objects[${i}].${k}`, errs); diff --git a/plans/sim-builder/PLAN.md b/plans/sim-builder/PLAN.md index f02c11e..d37b13b 100644 --- a/plans/sim-builder/PLAN.md +++ b/plans/sim-builder/PLAN.md @@ -62,7 +62,7 @@ | Phase 7: Classroom | fullstack | ✅ Done | ✅ | ✅ | ✅ | ## Final Review -- [ ] Comprehensive code review (final-reviewer) -- [ ] Security review (safe expression eval, ownership, sanitization) -- [ ] Full test suite passes (within baseline) -- [ ] Merged to `master` +- [x] Comprehensive code review (final-reviewer) — READY TO MERGE, 0 critical +- [x] Security review — движок выражений безопасен (нет eval/доступа к globals), ownership/IDOR ок, SQL параметризован; ФИКС: вайтлист цветов в validateSpec (CSS-инъекция `color/bg` через style.cssText закрыта) +- [x] Full test suite passes (within baseline) — custom-sims 39/39; общий 241/249 (8 = baseline: 3 auth + 5 jsdom page) +- [ ] Merged to `master` — ОЖИДАЕТ одобрения пользователя