feat(web+appview): CSS sanitization for theme cssOverrides (ATB-62) (#92)
* feat(web+appview): CSS sanitization for theme cssOverrides (ATB-62)
Add @atbb/css-sanitizer workspace package (css-tree v2 AST-based) that
strips dangerous CSS constructs — @import, external url(), @font-face
with external src, expression(), -moz-binding, behavior, data: URIs —
while preserving safe structural overrides.
- appview: sanitize cssOverrides at write time (POST + PUT /api/admin/themes)
and log any stripped constructs as structured warnings
- web: replace inline stub sanitizeCss with the real package; enable the
CSS overrides textarea in the theme editor (was disabled pending ATB-62)
* fix(css-sanitizer): address PR review security and quality issues
Critical:
- Strip </style> sequences from generated output to prevent HTML parser
breakout when CSS is injected via dangerouslySetInnerHTML (XSS regression)
- Fail closed on css-tree onParseError: Raw nodes from error recovery bypass
walker checks, so discard entire output when any parse error occurs
- Wrap sanitizeCssOverrides calls in dedicated try-catch in POST and PUT
theme handlers (separate from PDS write block per CLAUDE.md granularity rule)
- Add try-catch around sanitizeCss calls in BaseLayout with empty fallback
so a css-tree bug doesn't 500 every page for all users
Security:
- Sanitize cssOverrides in POST /api/admin/themes/:rkey/duplicate so
pre-sanitization records don't propagate dangerous CSS via duplication
- Move warning push after list.remove() so audit log only says "Stripped X"
when the node was actually removed (not before the null-check)
- Fix onParseError type signature: (error: SyntaxError) => void
Quality:
- Replace JSON.stringify(warnings) with warnings in structured logger calls
- Update Bruno Create Theme.bru: remove stale ATB-62 placeholder text
- Add integration tests: dangerous CSS stripped in POST and PUT theme handlers
- Fix duplicate test expectation: sanitizer now runs on duplication (compact form)
- Fix </style> test: split into fail-closed test and string-literal stripping test