fix(sim-builder): вайтлист цветов в validateSpec — закрыть CSS-инъекцию в шаренных спеках (финальное ревью)

This commit is contained in:
Maxim Dolgolyov
2026-06-13 13:33:14 +03:00
parent 9bd40c5d1c
commit d8717d0fbd
2 changed files with 33 additions and 6 deletions
+29 -2
View File
@@ -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);
+4 -4
View File
@@ -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` — ОЖИДАЕТ одобрения пользователя