openvibers/openvibe.media Commits

Unknown paths answer Express's own 404 page under Media's CSP, which now allows Cloudflare Web Analytics (browser check, WS-Q task 3): server/not-found.js keeps default-src 'none' and adds script-src https://static.cloudflareinsights.com (the beacon Cloudflare injects at the edge) and connect-src https://cloudflareinsights.com (where it reports). Media's pages send no policy of their own, and Express's default 404 policy blocked the beacon: a CSP error on every not-found page. The network's privacy text already says Cloudflare may measure performance. Tests: not-found-csp (new), observability (the real server's unknown path)

OP OpenVibers committed 26 Sept 2026 · b578bbba1c · 4 files, +99 −0


Changes

ddiff --git a/server/index.js b/server/index.js
iindex 86bd3f2..5b32f5f 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -185,6 +185,7 @@ app.use('/api/v2/:app/objects', require('./objects/routes')); // canonical obj
app.use('/api/v2/:app/jobs', require('./jobs/routes')); // media jobs: thumbnails, split/remux, invariant scans
app.use('/o', require('./objects/routes').publicRouter); // object bytes (public, or signed)
app.use('/', require('./public/routes')); // /v /c /p /t /f
++app.use(require('./not-found').notFound); // nothing matched: 404 with Media's CSP (Cloudflare's beacon allowed)
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
ddiff --git a/server/not-found.js b/server/not-found.js
nnew file mode 100644
iindex 0000000..d276652
---- /dev/null
++++ b/server/not-found.js
@@@ -0,0 +1,36 @@
++'use strict';
++/**
++ * The answer for a path nothing matched: Express's own 404 page ("Cannot GET /x", default-src 'none'),
++ * plus Cloudflare Web Analytics. Cloudflare injects its beacon into every HTML answer at the edge, and
++ * the network's privacy text says Cloudflare may measure performance; under Express's default policy the
++ * beacon was blocked and logged a CSP error on every not-found page (browser check, OpenVibe.Host
++ * scripts/browser-check.js). Media's pages send no policy of their own, so this is Media's one CSP.
++ */
++const CSP = [
++ "default-src 'none'",
++ // Cloudflare Web Analytics: script-src loads the beacon, connect-src is where it reports.
++ 'script-src https://static.cloudflareinsights.com',
++ 'connect-src https://cloudflareinsights.com',
++].join('; ');
++
++const ESC = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
++const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ESC[c]);
++
++function notFound(req, res) {
++ res.status(404);
++ res.set('Content-Security-Policy', CSP);
++ res.set('X-Content-Type-Options', 'nosniff');
++ res.type('html').send(`<!DOCTYPE html>
++<html lang="en">
++<head>
++<meta charset="utf-8">
++<title>Error</title>
++</head>
++<body>
++<pre>Cannot ${esc(req.method)} ${esc(req.originalUrl)}</pre>
++</body>
++</html>
++`);
++}
++
++module.exports = { notFound, CSP };
ddiff --git a/test/not-found-csp.test.js b/test/not-found-csp.test.js
nnew file mode 100644
iindex 0000000..23637b0
---- /dev/null
++++ b/test/not-found-csp.test.js
@@@ -0,0 +1,56 @@
++'use strict';
++// server/not-found.js: an unknown path gets Express's own 404 page ("Cannot GET /x") under Media's CSP,
++// which is default-src 'none' plus Cloudflare Web Analytics (script-src static.cloudflareinsights.com,
++// connect-src cloudflareinsights.com). Cloudflare injects the beacon at the edge; Express's default
++// policy blocked it and logged a CSP error on every not-found page (browser check, OpenVibe.Host).
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const http = require('http');
++const express = require('express');
++const { notFound, CSP } = require('../server/not-found');
++
++function send(base, p, method = 'GET') {
++ return new Promise((resolve, reject) => {
++ const req = http.request(base + p, { method, headers: { accept: 'text/html' } }, (res) => {
++ let b = ''; res.on('data', (d) => { b += d; });
++ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: b }));
++ });
++ req.on('error', reject); req.end();
++ });
++}
++
++(async () => {
++ const app = express();
++ app.get('/known', (req, res) => res.send('ok'));
++ app.use(notFound);
++ const server = await new Promise((resolve) => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });
++ const base = `http://127.0.0.1:${server.address().port}`;
++ try {
++ assert.strictEqual(CSP, "default-src 'none'; script-src https://static.cloudflareinsights.com; connect-src https://cloudflareinsights.com");
++ assert.strictEqual((await send(base, '/known')).status, 200);
++
++ const r = await send(base, '/__ovcheck-404');
++ assert.strictEqual(r.status, 404);
++ assert.strictEqual(r.headers['content-security-policy'], CSP);
++ assert.strictEqual(r.headers['x-content-type-options'], 'nosniff');
++ assert.match(r.headers['content-type'], /^text\/html/);
++ assert.ok(r.body.includes('<pre>Cannot GET /__ovcheck-404</pre>'), r.body);
++
++ const x = await send(base, '/x%3Cb%3E?q=<script>');
++ assert.strictEqual(x.status, 404);
++ assert.ok(!x.body.includes('<script>'), 'the path is escaped');
++
++ const post = await send(base, '/nope', 'POST');
++ assert.ok(post.body.includes('<pre>Cannot POST /nope</pre>'));
++ const head = await send(base, '/nope', 'HEAD');
++ assert.deepStrictEqual([head.status, head.body], [404, '']);
++
++ // index.js mounts it after every route and before the error handler.
++ const idx = fs.readFileSync(path.join(__dirname, '..', 'server', 'index.js'), 'utf8');
++ const at = idx.indexOf("app.use(require('./not-found').notFound)");
++ assert.ok(at > idx.indexOf("app.use('/', require('./public/routes'))"), 'after the last routes');
++ assert.ok(at < idx.indexOf('app.use((err, req, res, next) =>'), 'before the error handler');
++ console.log('not-found: 404 under Media\'s CSP, Cloudflare Web Analytics allowed');
++ } finally { server.close(); }
++})().catch((e) => { console.error(e); process.exit(1); });
ddiff --git a/test/observability.test.js b/test/observability.test.js
iindex ba7f5a2..33fb2f0 100644
---- a/test/observability.test.js
++++ b/test/observability.test.js
@@@ -146,6 +146,12 @@ const listen = (app) => new Promise((resolve) => { const s = app.listen(0, '127.
assert.ok(m.body.includes('http_requests_total{method="GET",route="/api/ready",status_class="2xx"} 1\n'), m.body.slice(0, 400));
const h = await request(b2, '/healthz');
assert.strictEqual(h.status, 200, '/healthz still answers');
++ // An unknown path is index.js's last handler (server/not-found.js): a 404 whose CSP lets Cloudflare's beacon run.
++ const nf = await new Promise((resolve, reject) => http.get(`${b2}/__ovcheck-404`, { headers: { accept: 'text/html' } }, (res) => {
++ let b = ''; res.on('data', (d) => { b += d; }); res.on('end', () => resolve({ status: res.statusCode, csp: res.headers['content-security-policy'], body: b }));
++ }).on('error', reject));
++ assert.deepStrictEqual([nf.status, nf.csp], [404, require('../server/not-found').CSP]);
++ assert.ok(nf.body.includes('<pre>Cannot GET /__ovcheck-404</pre>'));
// The release manifest (registry.release-manifest@1) names where open tabs report updates,
// and those reports land in /metrics.
const rel = await request(b2, '/release.json');