openvibers/openvibe.live Commits

offline-html-sanitize: escape < > in offline_css (srcdoc style-block breakout)

OP OpenVibers committed 21 Sept 2026 · e6afceeee1 · 1 file, +9 −4

offline_css is interpolated verbatim into <style>\${css}</style> in the iframe srcdoc string
built by app-channel.js — that's an HTML document context, not a CSS context, and the regex
sanitizer only stripped CSS-specific constructs (@import, expression(), etc). A value
containing "</style><script>...</script>" closed the style element early and opened a real
<script> element in the srcdoc. Currently inert (the iframe sandbox no longer grants
allow-scripts), but a real HTML-structure injection regardless — it could also inject a <base
href> to hijack how the img-proxy's origin-relative URLs resolve. CSS never needs a literal <
or >, so both are escaped to their CSS unicode-escape form, which parses identically for any
real stylesheet.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMxJm1dB8rdsaMSgNjVa82

Changes

ddiff --git a/server/streaming/offline-html-sanitize.js b/server/streaming/offline-html-sanitize.js
iindex 7628fc8d..da2a8a9e 100644
---- a/server/streaming/offline-html-sanitize.js
++++ b/server/streaming/offline-html-sanitize.js
@@@ -70,9 +70,12 @@ function sanitizeOfflineHtml(html) {
return sanitizeHtml(s, HTML_OPTIONS);
}
--// offline_css is a standalone <style> block, never inline script-bearing markup — but strip the
--// handful of constructs that historically executed code from CSS (dead in modern engines, kept
--// as defense in depth) and any @import (would be a first-party-bypassing, untracked fetch).
++// offline_css is interpolated verbatim into `<style>${css}</style>` inside the iframe's srcdoc
++// (app-channel.js) — a plain string template, not a real DOM API, so it is an HTML *document*
++// context, not just a CSS context. A value containing "</style><script>…" would close the style
++// element early and open a real (if currently sandbox-inert) <script> element; a "<base href=…>"
++// could hijack how the proxied, origin-relative img-proxy URLs resolve. CSS never legitimately
++// needs a literal "<" or ">", so both are escaped — this can't break any valid stylesheet.
function sanitizeOfflineCss(css) {
const s = String(css || '');
if (!s.trim()) return '';
@@@ -80,7 +83,9 @@ function sanitizeOfflineCss(css) {
.replace(/@import[^;]*;?/gi, '')
.replace(/expression\s*\(/gi, 'blocked(')
.replace(/-moz-binding\s*:/gi, 'blocked:')
-- .replace(/behavior\s*:/gi, 'blocked:');
++ .replace(/behavior\s*:/gi, 'blocked:')
++ .replace(/</g, '\\3C ')
++ .replace(/>/g, '\\3E ');
}
module.exports = { sanitizeOfflineHtml, sanitizeOfflineCss };