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:
|
||||||
|
- htmlcss: sandbox="" (empty) + meta CSP → scripts totalement inertes
|
||||||
Sandbox policy (never deviate):
|
- js: sandbox avec tous les tokens SAUF allow-same-origin
|
||||||
- htmlcss tier: sandbox="" (empty) → scripts are INERT. A meta CSP reinforces this.
|
→ scripts libres, fetch vers l'extérieur OK, accès parent impossible
|
||||||
- js tier: sandbox="allow-scripts" ONLY → script runs in a NULL origin and
|
(null origin = isolation réelle sans allow-same-origin)
|
||||||
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.
|
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="rich-frame-wrap">
|
<div class="rich-frame-wrap">
|
||||||
@@ -20,6 +13,7 @@
|
|||||||
{{ mode === 'js' ? '⚡ JS' : '🎨 HTML/CSS' }} · bac à sable
|
{{ mode === 'js' ? '⚡ JS' : '🎨 HTML/CSS' }} · bac à sable
|
||||||
</span>
|
</span>
|
||||||
<iframe
|
<iframe
|
||||||
|
ref="frameRef"
|
||||||
class="rich-frame"
|
class="rich-frame"
|
||||||
:sandbox="sandboxTokens"
|
:sandbox="sandboxTokens"
|
||||||
:srcdoc="srcdoc"
|
:srcdoc="srcdoc"
|
||||||
@@ -31,30 +25,55 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, useTemplateRef, watchEffect } from 'vue';
|
||||||
|
|
||||||
const props = defineProps<{ mode: 'htmlcss' | 'js'; content: string }>();
|
const props = defineProps<{ mode: 'htmlcss' | 'js'; content: string }>();
|
||||||
|
|
||||||
// htmlcss → no scripts at all; js → scripts only, NEVER same-origin.
|
const frameRef = useTemplateRef<HTMLIFrameElement>('frameRef');
|
||||||
const sandboxTokens = computed(() => (props.mode === 'js' ? 'allow-scripts' : ''));
|
|
||||||
|
|
||||||
// Defense-in-depth assertion: the iframe must never get allow-same-origin alongside scripts.
|
// htmlcss → aucun script ; js → tout permis sauf accès au parent (pas de allow-same-origin)
|
||||||
if (import.meta.env.DEV) {
|
const sandboxTokens = computed(() =>
|
||||||
const t = props.mode === 'js' ? 'allow-scripts' : '';
|
props.mode === 'js'
|
||||||
if (t.includes('allow-same-origin') && t.includes('allow-scripts')) {
|
? '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');
|
throw new Error('SECURITY: rich iframe must never combine allow-scripts + allow-same-origin');
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
const srcdoc = computed(() => {
|
const srcdoc = computed(() => {
|
||||||
// For htmlcss mode: meta CSP blocks scripts as a second layer (sandbox="" already blocks them too).
|
// htmlcss : meta CSP en second couche (le sandbox="" bloque déjà les scripts)
|
||||||
// For js mode: NO meta CSP — the null-origin sandbox (allow-scripts without allow-same-origin)
|
// js : pas de meta CSP — le sandbox null-origin est la vraie frontière
|
||||||
// is the real security boundary; adding a meta CSP with default-src 'none' in a null-origin
|
const metaCsp = props.mode === 'htmlcss'
|
||||||
// srcdoc iframe causes Chromium to silently block inline scripts despite 'unsafe-inline'.
|
? `<meta http-equiv="Content-Security-Policy"
|
||||||
const metaCsp = props.mode === 'js'
|
content="default-src 'none'; style-src 'unsafe-inline'; img-src data: https:; font-src data:;">`
|
||||||
? ''
|
: '';
|
||||||
: '<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>`;
|
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>
|
</script>
|
||||||
|
|
||||||
@@ -75,9 +94,8 @@ const srcdoc = computed(() => {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
.rich-tag--htmlcss { color: #00ddaa; background: #062019; border: 1px solid #0a4435; }
|
.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 {
|
.rich-frame {
|
||||||
width: 480px;
|
width: 480px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user