This commit is contained in:
@@ -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%;
|
||||
|
||||
Reference in New Issue
Block a user