openvibers/openvibe.network Commits

CORS: a refused origin gets a 403 JSON answer (code cors.origin_denied) instead of Express's default 500 with a stack trace on every request, and is logged once an hour per origin (a local test page on a random 127.0.0.1 port produced ~800 stack traces an hour). Still refused server-side, so a cross-site form post never runs the route. public-cors.js originGuard() is the one implementation index.js and the test use

OP OpenVibers committed 23 Sept 2026 · ca59964caf · 3 files, +38 −15


Changes

ddiff --git a/server/index.js b/server/index.js
iindex 4a147efb..12668420 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -272,15 +272,9 @@ function isAllowedOrigin(origin) {
// Public discovery (/.well-known/openvibe, /api/v1/registry/*, /contracts/*.json) answers any
// origin, preflight included; every other route keeps this allow-list (server/public-cors.js).
--app.use(require('./public-cors').gate(cors({
-- origin(origin, callback) {
-- if (!origin) return callback(null, true); // non-browser / server-to-server
-- if (isAllowedOrigin(origin)) return callback(null, true);
-- console.warn(`[CORS] Rejected origin: "${origin}" | allowed set: ${[...buildAllowedOriginsSet()].join(', ')} | subdomainBase: ${getToolsSubdomainBase()}`);
-- return callback(new Error('Origin not allowed by CORS'));
-- },
-- credentials: true,
--})));
++const corsGuard = require('./public-cors').originGuard(isAllowedOrigin);
++app.use(require('./public-cors').gate(cors({ origin: corsGuard.origin, credentials: true })));
++app.use(corsGuard.denied);
// ── Rate Limiting ────────────────────────────────────────────
app.use('/api/', rateLimit({ windowMs: 60_000, max: 120 }));
ddiff --git a/server/public-cors.js b/server/public-cors.js
iindex 4b20e634..ae3876f8 100644
---- a/server/public-cors.js
++++ b/server/public-cors.js
@@@ -57,4 +57,32 @@ function gate(restricted) {
};
}
--module.exports = { gate, publicCors, isPublicDiscoveryPath, ALLOW_HEADERS, EXPOSE_HEADERS };
++/**
++ * The allow-list's `origin` option for cors(), plus the handler for what it refuses. A foreign
++ * origin is still refused server-side (so a cross-site form post never runs the route), but as a
++ * 403 JSON answer instead of Express's default 500 with a stack trace per request, and each
++ * refused origin is logged at most once an hour (a local renderer on a random port asked
++ * hundreds of times an hour).
++ */
++function originGuard(isAllowed, { log = console, everyMs = 60 * 60 * 1000 } = {}) {
++ const seen = new Map();
++ function origin(o, callback) {
++ if (!o || isAllowed(o)) return callback(null, true); // no Origin: non-browser / server-to-server
++ const now = Date.now();
++ if (!(seen.get(o) > now - everyMs)) {
++ if (seen.size > 1000) seen.clear();
++ seen.set(o, now);
++ log.warn(`[CORS] Refused origin "${o}" (logged once an hour per origin)`);
++ }
++ const err = new Error('Origin not allowed by CORS');
++ err.code = 'cors.origin_denied';
++ return callback(err);
++ }
++ function denied(err, req, res, next) {
++ if (!err || err.code !== 'cors.origin_denied') return next(err);
++ res.status(403).json({ error: 'origin not allowed', code: 'cors.origin_denied' });
++ }
++ return { origin, denied };
++}
++
++module.exports = { gate, publicCors, isPublicDiscoveryPath, originGuard, ALLOW_HEADERS, EXPOSE_HEADERS };
ddiff --git a/test/public-cors.test.js b/test/public-cors.test.js
iindex c4aeba42..02aa5262 100644
---- a/test/public-cors.test.js
++++ b/test/public-cors.test.js
@@@ -25,10 +25,10 @@ const { createEcosystemRegistry } = require('../server/registry/ecosystem');
const allowed = new Set(['https://openvibe.live']);
const eco = createEcosystemRegistry({ issuer: 'https://openvibe.network', internalOverrides: {} });
const app = express();
-- app.use(publicCors.gate(cors({
-- origin(origin, cb) { if (!origin || allowed.has(origin)) return cb(null, true); return cb(new Error('Origin not allowed by CORS')); },
-- credentials: true,
-- })));
++ const warned = [];
++ const guard = publicCors.originGuard((o) => allowed.has(o), { log: { warn: (m) => warned.push(m) } });
++ app.use(publicCors.gate(cors({ origin: guard.origin, credentials: true })));
++ app.use(guard.denied);
app.use(eco.router());
app.post('/api/auth/login', (_req, res) => res.json({ ok: true }));
app.get('/api/v1/projects', (_req, res) => res.json({ projects: [] }));
@@@ -74,8 +74,9 @@ const { createEcosystemRegistry } = require('../server/registry/ecosystem');
r = await req(method, p, { origin: FOREIGN, 'access-control-request-method': 'POST' });
assert.notStrictEqual(r.headers.get('access-control-allow-origin'), '*', `${method} ${p} is not opened`);
assert.notStrictEqual(r.headers.get('access-control-allow-origin'), FOREIGN, `${method} ${p} does not reflect a foreign origin`);
-- assert.strictEqual(r.status, 500, `${method} ${p} rejects the foreign origin`);
++ assert.strictEqual(r.status, 403, `${method} ${p} refuses the foreign origin (the route never runs)`);
}
++ assert.strictEqual(warned.length, 1, 'a refused origin is logged once, not per request');
// ...while first-party origins still work there, with credentials.
r = await req('GET', '/api/v1/projects', { origin: 'https://openvibe.live' });
assert.strictEqual(r.status, 200);