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
Changes
@@@ -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 }));@@@ -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 };@@@ -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);
@@ -272,15 +272,9 @@ function isAllowedOrigin(origin) {
@@ -272,15 +272,9 @@ function isAllowedOrigin(origin) {
// Public discovery (/.well-known/openvibe, /api/v1/registry/*, /contracts/*.json) answers any
// 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).
// 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 ────────────────────────────────────────────
// ── Rate Limiting ────────────────────────────────────────────
app.use('/api/', rateLimit({ windowMs: 60_000, max: 120 }));
app.use('/api/', rateLimit({ windowMs: 60_000, max: 120 }));
@@ -57,4 +57,32 @@ function gate(restricted) {
@@ -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 };
@@ -25,10 +25,10 @@ const { createEcosystemRegistry } = require('../server/registry/ecosystem');
@@ -25,10 +25,10 @@ const { createEcosystemRegistry } = require('../server/registry/ecosystem');
const allowed = new Set(['https://openvibe.live']);
const allowed = new Set(['https://openvibe.live']);
const eco = createEcosystemRegistry({ issuer: 'https://openvibe.network', internalOverrides: {} });
const eco = createEcosystemRegistry({ issuer: 'https://openvibe.network', internalOverrides: {} });
const app = express();
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.use(eco.router());
app.post('/api/auth/login', (_req, res) => res.json({ ok: true }));
app.post('/api/auth/login', (_req, res) => res.json({ ok: true }));
app.get('/api/v1/projects', (_req, res) => res.json({ projects: [] }));
app.get('/api/v1/projects', (_req, res) => res.json({ projects: [] }));
@@ -74,8 +74,9 @@ const { createEcosystemRegistry } = require('../server/registry/ecosystem');
@@ -74,8 +74,9 @@ const { createEcosystemRegistry } = require('../server/registry/ecosystem');
r = await req(method, p, { origin: FOREIGN, 'access-control-request-method': 'POST' });
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'), '*', `${method} ${p} is not opened`);
assert.notStrictEqual(r.headers.get('access-control-allow-origin'), FOREIGN, `${method} ${p} does not reflect a foreign origin`);
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.
// ...while first-party origins still work there, with credentials.
r = await req('GET', '/api/v1/projects', { origin: 'https://openvibe.live' });
r = await req('GET', '/api/v1/projects', { origin: 'https://openvibe.live' });
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);