openvibers/openvibe.search Commits

Search's CSP allows its own web manifest (manifest-src 'self'; default-src 'none' refused it in every console) (#86)

OP OpenVibers committed 20 days ago · 568f5fa564 · 2 files, +4 −1

Changes

ddiff --git a/server/web/page.js b/server/web/page.js
iindex 5aa0352..65c8bf8 100644
---- a/server/web/page.js
++++ b/server/web/page.js
@@@ -50,11 +50,13 @@ const FRAME = {
};
const FOOTER = { service: 'search', variant: 'compact', mount: '#ov-footer', brandName: SITE_NAME, updates: '/updates' };
const FOOTER_INIT = `window.addEventListener('DOMContentLoaded', function () { try { OpenVibeFooter.init(${JSON.stringify(FOOTER).replace(/</g, '\\u003c')}); } catch (e) { /* the Frame is optional */ } });`;
++// manifest-src 'self': the page links its own web manifest (/shared/manifest/search.webmanifest), which default-src 'none'
++// refused in every visitor's console (2026-10-24).
// The two inline scripts are constant (the shell's boot depends only on FRAME), so script-src allows exactly
// them by hash and still has no 'unsafe-inline'.
const sha256 = (js) => `'sha256-${require('crypto').createHash('sha256').update(js, 'utf8').digest('base64')}'`;
const INLINE = [...shell.scripts(FRAME).matchAll(/<script>([\s\S]*?)<\/script>/g)].map((m) => m[1]).concat(FOOTER_INIT);
--const CSP = `default-src 'none'; script-src 'self' ${NETWORK} ${CF_BEACON} ${INLINE.map(sha256).join(' ')}; connect-src 'self' ${NETWORK} ${CF_REPORT} ${EVENTS}; style-src 'self' 'unsafe-inline' ${NETWORK}; img-src 'self' data: https:; frame-src ${NETWORK}; form-action 'self' ${NETWORK}; base-uri 'none'; frame-ancestors 'none'`;
++const CSP = `default-src 'none'; script-src 'self' ${NETWORK} ${CF_BEACON} ${INLINE.map(sha256).join(' ')}; connect-src 'self' ${NETWORK} ${CF_REPORT} ${EVENTS}; style-src 'self' 'unsafe-inline' ${NETWORK}; img-src 'self' data: https:; manifest-src 'self'; frame-src ${NETWORK}; form-action 'self' ${NETWORK}; base-uri 'none'; frame-ancestors 'none'`;
const ICON_LINKS = require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: `${NETWORK}/assets` }).split('\n').filter((l) => l.startsWith('<link') && !/rel="manifest"/.test(l)).join('\n');
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
ddiff --git a/test/page.test.js b/test/page.test.js
iindex 28732e5..e708697 100644
---- a/test/page.test.js
++++ b/test/page.test.js
@@@ -42,6 +42,7 @@ t('the front page is a search form, indexable, no-store, with a strict CSP', asy
assert.match(csp, /script-src 'self' https:\/\/openvibe\.network https:\/\/static\.cloudflareinsights\.com 'sha256-[^']+'/);
// And the Events realtime stream, for release notifications (release-watch, openvibe-shared 1.17).
assert.match(csp, /connect-src 'self' https:\/\/openvibe\.network https:\/\/cloudflareinsights\.com https:\/\/openvibe\.events;/);
++ assert.match(csp, /manifest-src 'self';/, 'its own web manifest loads (default-src none refused it)');
assert.ok(!/script-src[^;]*unsafe-inline/.test(csp), 'no inline script allowed by default');
const allowed = [...(/script-src ([^;]*)/.exec(csp)[1]).matchAll(/'sha256-([^']+)'/g)].map((m) => m[1]);
const scripts = [...r.text.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi)];