external sources
All checks were successful
Deploy XIP / deploy (push) Successful in 46s

This commit is contained in:
raphael.thieffry
2026-05-31 17:10:41 +02:00
parent 366d4e8f8b
commit 942fcaa4d1

View File

@@ -1,18 +1,11 @@
<!--
Rich message renderer SECURITY CRITICAL.
Rich message renderer.
Renders paid HTML/CSS or JS messages inside a FIXED-SIZE sandboxed iframe.
Sandbox policy (never deviate):
- htmlcss tier: sandbox="" (empty) scripts are INERT. A meta CSP reinforces this.
- js tier: sandbox="allow-scripts" ONLY script runs in a NULL origin and
cannot touch the parent (no allow-same-origin, ever).
NO meta CSP for js mode: Chromium silently blocks 'unsafe-inline'
in null-origin srcdoc iframes despite it being declared; the sandbox
without allow-same-origin is the real isolation boundary.
We NEVER combine allow-scripts with allow-same-origin (that would re-grant parent
access and defeat isolation). A runtime assertion below guards against it.
Sandbox policy:
- htmlcss: sandbox="" (empty) + meta CSP scripts totalement inertes
- js: sandbox avec tous les tokens SAUF allow-same-origin
scripts libres, fetch vers l'extérieur OK, accès parent impossible
(null origin = isolation réelle sans allow-same-origin)
-->
<template>
<div class="rich-frame-wrap">
@@ -20,6 +13,7 @@
{{ mode === 'js' ? ' JS' : '🎨 HTML/CSS' }} · bac à sable
</span>
<iframe
ref="frameRef"
class="rich-frame"
:sandbox="sandboxTokens"
:srcdoc="srcdoc"
@@ -31,30 +25,55 @@
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { computed, useTemplateRef, watchEffect } from 'vue';
const props = defineProps<{ mode: 'htmlcss' | 'js'; content: string }>();
// htmlcss → no scripts at all; js → scripts only, NEVER same-origin.
const sandboxTokens = computed(() => (props.mode === 'js' ? 'allow-scripts' : ''));
const frameRef = useTemplateRef<HTMLIFrameElement>('frameRef');
// Defense-in-depth assertion: the iframe must never get allow-same-origin alongside scripts.
if (import.meta.env.DEV) {
const t = props.mode === 'js' ? 'allow-scripts' : '';
if (t.includes('allow-same-origin') && t.includes('allow-scripts')) {
// htmlcss → aucun script ; js → tout permis sauf accès au parent (pas de allow-same-origin)
const sandboxTokens = computed(() =>
props.mode === 'js'
? 'allow-scripts allow-forms allow-modals allow-downloads allow-popups allow-presentation allow-pointer-lock'
: ''
);
// Garde de sécurité réactive — allow-scripts + allow-same-origin = catastrophe
watchEffect(() => {
const tokens = sandboxTokens.value;
if (tokens.includes('allow-scripts') && tokens.includes('allow-same-origin')) {
throw new Error('SECURITY: rich iframe must never combine allow-scripts + allow-same-origin');
}
}
});
const srcdoc = computed(() => {
// For htmlcss mode: meta CSP blocks scripts as a second layer (sandbox="" already blocks them too).
// For js mode: NO meta CSP — the null-origin sandbox (allow-scripts without allow-same-origin)
// is the real security boundary; adding a meta CSP with default-src 'none' in a null-origin
// srcdoc iframe causes Chromium to silently block inline scripts despite 'unsafe-inline'.
const metaCsp = props.mode === 'js'
? ''
: '<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; script-src \'none\'; style-src \'unsafe-inline\'; img-src data: https:; font-src data:;">';
return `<!doctype html><html><head><meta charset="utf-8">${metaCsp}<style>html,body{margin:0;padding:8px;color:#ddd;font-family:Arial,sans-serif;background:#0a0a12;overflow:auto;height:100%;box-sizing:border-box}</style></head><body>${props.content}</body></html>`;
// htmlcss : meta CSP en second couche (le sandbox="" bloque déjà les scripts)
// js : pas de meta CSP — le sandbox null-origin est la vraie frontière
const metaCsp = props.mode === 'htmlcss'
? `<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; style-src 'unsafe-inline'; img-src data: https:; font-src data:;">`
: '';
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
${metaCsp}
<style>
html, body {
margin: 0;
padding: 8px;
color: #ddd;
font-family: Arial, sans-serif;
background: #0a0a12;
overflow: auto;
height: 100%;
box-sizing: border-box;
}
</style>
</head>
<body>${props.content}</body>
</html>`;
});
</script>
@@ -75,9 +94,8 @@ const srcdoc = computed(() => {
border-radius: 6px;
}
.rich-tag--htmlcss { color: #00ddaa; background: #062019; border: 1px solid #0a4435; }
.rich-tag--js { color: #ffcc44; background: #201a06; border: 1px solid #443a0a; }
.rich-tag--js { color: #ffcc44; background: #201a06; border: 1px solid #443a0a; }
/* Fixed size per README ("taille fixe") — contains any layout-breaking CSS. */
.rich-frame {
width: 480px;
max-width: 100%;