openvibers/openvibe.bench Commits
Render origin: the wrapper may frame the model document (#21)
Changes
@@@ -33,6 +33,8 @@ function wrapperCsp(nonce) {return "default-src 'none'; script-src 'nonce-" + nonce + "'; style-src 'unsafe-inline'; frame-src " + RENDER_ORIGIN+ "; img-src data:; connect-src 'none'; form-action 'none'; base-uri 'none'; frame-ancestors " + FRAME_ANCESTORS;}++/** The policy as a <meta> tag carries it: browsers ignore frame-ancestors there (and warn); the header has it. */++const metaPolicy = (p) => p.split('; ').filter((d) => !d.startsWith('frame-ancestors')).join('; ');/** The sandbox attributes for every model frame: scripts yes, everything else no. */const SANDBOX = 'allow-scripts';/** The default wall-clock a kind may run before the wrapper stops it. */@@@ -55,7 +57,8 @@ function cspFor(nonce) {"object-src 'none'","form-action 'none'","base-uri 'none'",-- "frame-ancestors " + FRAME_ANCESTORS,++ // The model document's parent is the wrapper, on the render origin itself; every ancestor must be listed.++ "frame-ancestors " + RENDER_ORIGIN + ' ' + FRAME_ANCESTORS,].join('; ');}@@@ -98,7 +101,7 @@ function modeOf(kind) {function head(nonce, extra) {return '<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">'-- + '<meta http-equiv="Content-Security-Policy" content="' + esc(cspFor(nonce)) + '">' + (extra || '');++ + '<meta http-equiv="Content-Security-Policy" content="' + esc(metaPolicy(cspFor(nonce))) + '">' + (extra || '');}/** The import map the wrapper injects: three.js is vendored on the render origin, never fetched from a CDN. */@@@ -186,7 +189,7 @@ function wrapperDocument(opts) {const nonce = (opts && opts.nonce) || crypto.randomBytes(16).toString('base64');const secs = Math.max(1, Math.round(limitMs / 1000));return '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">'-- + '<meta http-equiv="Content-Security-Policy" content="' + esc(wrapperCsp(nonce)) + '">'++ + '<meta http-equiv="Content-Security-Policy" content="' + esc(metaPolicy(wrapperCsp(nonce))) + '">'+ '<style>html,body{margin:0;height:100%;font:14px/1.4 system-ui,sans-serif;background:#0f1115;color:#e9edf2}'+ '.bar{display:flex;gap:.5rem;align-items:center;justify-content:space-between;padding:.35rem .6rem;background:#171a21;border-bottom:1px solid #262b36}'+ '.bar b{font-weight:600}.clock{font-variant-numeric:tabular-nums;color:#9fb0c8}'@@@ -98,6 +98,9 @@ const HOSTILE = [assert.ok(w.text.includes('src="' + renderLib.RENDER_ORIGIN + '/render/e/'), 'the child loads from the render origin');assert.ok(!w.headers.get('x-frame-options'), 'no X-Frame-Options DENY: frame-ancestors decides');assert.ok(wcsp.includes('frame-ancestors https://bench.openvibe.services'), 'the showcase page may frame it');++ const ecsp = (await t.get('/render/e/' + entryA)).headers.get('content-security-policy') || '';++ assert.ok(ecsp.includes('frame-ancestors ' + renderLib.RENDER_ORIGIN), 'the wrapper on the render origin may frame the model document: ' + ecsp);++ assert.ok(!/<meta[^>]*frame-ancestors/.test(w.text), 'no frame-ancestors in a meta tag (ignored there)');});await check('built-in pages and whole pages: one nonce per script, the import map for three and its addons', async () => {
@@ -33,6 +33,8 @@ function wrapperCsp(nonce) {
@@ -33,6 +33,8 @@ function wrapperCsp(nonce) {
return "default-src 'none'; script-src 'nonce-" + nonce + "'; style-src 'unsafe-inline'; frame-src " + RENDER_ORIGIN
return "default-src 'none'; script-src 'nonce-" + nonce + "'; style-src 'unsafe-inline'; frame-src " + RENDER_ORIGIN
+ "; img-src data:; connect-src 'none'; form-action 'none'; base-uri 'none'; frame-ancestors " + FRAME_ANCESTORS;
+ "; img-src data:; connect-src 'none'; form-action 'none'; base-uri 'none'; frame-ancestors " + FRAME_ANCESTORS;
}
}
+/** The policy as a <meta> tag carries it: browsers ignore frame-ancestors there (and warn); the header has it. */
+const metaPolicy = (p) => p.split('; ').filter((d) => !d.startsWith('frame-ancestors')).join('; ');
/** The sandbox attributes for every model frame: scripts yes, everything else no. */
/** The sandbox attributes for every model frame: scripts yes, everything else no. */
const SANDBOX = 'allow-scripts';
const SANDBOX = 'allow-scripts';
/** The default wall-clock a kind may run before the wrapper stops it. */
/** The default wall-clock a kind may run before the wrapper stops it. */
@@ -55,7 +57,8 @@ function cspFor(nonce) {
@@ -55,7 +57,8 @@ function cspFor(nonce) {
"object-src 'none'",
"object-src 'none'",
"form-action 'none'",
"form-action 'none'",
"base-uri 'none'",
"base-uri 'none'",
- "frame-ancestors " + FRAME_ANCESTORS,
+ // The model document's parent is the wrapper, on the render origin itself; every ancestor must be listed.
+ "frame-ancestors " + RENDER_ORIGIN + ' ' + FRAME_ANCESTORS,
].join('; ');
].join('; ');
}
}
@@ -98,7 +101,7 @@ function modeOf(kind) {
@@ -98,7 +101,7 @@ function modeOf(kind) {
function head(nonce, extra) {
function head(nonce, extra) {
return '<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">'
return '<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">'
- + '<meta http-equiv="Content-Security-Policy" content="' + esc(cspFor(nonce)) + '">' + (extra || '');
+ + '<meta http-equiv="Content-Security-Policy" content="' + esc(metaPolicy(cspFor(nonce))) + '">' + (extra || '');
}
}
/** The import map the wrapper injects: three.js is vendored on the render origin, never fetched from a CDN. */
/** The import map the wrapper injects: three.js is vendored on the render origin, never fetched from a CDN. */
@@ -186,7 +189,7 @@ function wrapperDocument(opts) {
@@ -186,7 +189,7 @@ function wrapperDocument(opts) {
const nonce = (opts && opts.nonce) || crypto.randomBytes(16).toString('base64');
const nonce = (opts && opts.nonce) || crypto.randomBytes(16).toString('base64');
const secs = Math.max(1, Math.round(limitMs / 1000));
const secs = Math.max(1, Math.round(limitMs / 1000));
return '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">'
return '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">'
- + '<meta http-equiv="Content-Security-Policy" content="' + esc(wrapperCsp(nonce)) + '">'
+ + '<meta http-equiv="Content-Security-Policy" content="' + esc(metaPolicy(wrapperCsp(nonce))) + '">'
+ '<style>html,body{margin:0;height:100%;font:14px/1.4 system-ui,sans-serif;background:#0f1115;color:#e9edf2}'
+ '<style>html,body{margin:0;height:100%;font:14px/1.4 system-ui,sans-serif;background:#0f1115;color:#e9edf2}'
+ '.bar{display:flex;gap:.5rem;align-items:center;justify-content:space-between;padding:.35rem .6rem;background:#171a21;border-bottom:1px solid #262b36}'
+ '.bar{display:flex;gap:.5rem;align-items:center;justify-content:space-between;padding:.35rem .6rem;background:#171a21;border-bottom:1px solid #262b36}'
+ '.bar b{font-weight:600}.clock{font-variant-numeric:tabular-nums;color:#9fb0c8}'
+ '.bar b{font-weight:600}.clock{font-variant-numeric:tabular-nums;color:#9fb0c8}'
@@ -98,6 +98,9 @@ const HOSTILE = [
@@ -98,6 +98,9 @@ const HOSTILE = [
assert.ok(w.text.includes('src="' + renderLib.RENDER_ORIGIN + '/render/e/'), 'the child loads from the render origin');
assert.ok(w.text.includes('src="' + renderLib.RENDER_ORIGIN + '/render/e/'), 'the child loads from the render origin');
assert.ok(!w.headers.get('x-frame-options'), 'no X-Frame-Options DENY: frame-ancestors decides');
assert.ok(!w.headers.get('x-frame-options'), 'no X-Frame-Options DENY: frame-ancestors decides');
assert.ok(wcsp.includes('frame-ancestors https://bench.openvibe.services'), 'the showcase page may frame it');
assert.ok(wcsp.includes('frame-ancestors https://bench.openvibe.services'), 'the showcase page may frame it');
+ const ecsp = (await t.get('/render/e/' + entryA)).headers.get('content-security-policy') || '';
+ assert.ok(ecsp.includes('frame-ancestors ' + renderLib.RENDER_ORIGIN), 'the wrapper on the render origin may frame the model document: ' + ecsp);
+ assert.ok(!/<meta[^>]*frame-ancestors/.test(w.text), 'no frame-ancestors in a meta tag (ignored there)');
});
});
await check('built-in pages and whole pages: one nonce per script, the import map for three and its addons', async () => {
await check('built-in pages and whole pages: one nonce per script, the import map for three and its addons', async () => {