openvibers/openvibe.network Commits

SSO check: ov_sso never authenticates API requests (CSRF), origin allow-list is the owned zones only

OP OpenVibers committed 18 Sept 2026 · d34d8ce83f · 3 files, +13 −3


Changes

ddiff --git a/server/auth/session.js b/server/auth/session.js
iindex 48df257d..eecdfe40 100644
---- a/server/auth/session.js
++++ b/server/auth/session.js
@@@ -56,7 +56,10 @@ function clearSessionCookies(res) {
res.clearCookie('ov_token', { path: '/', sameSite: 'Lax', secure: true });
res.clearCookie('ov_sso', { path: '/', sameSite: 'None', secure: true, httpOnly: true });
}
--/** The session token a request carries: Bearer header, then the page cookie, then the cross-site one. */
++/**
++ * The session token a request carries: Bearer header, then the page cookie, then the cross-site
++ * one. Only for endpoints that change nothing (/sso/check) — see makeRequireAuth for why.
++ */
function requestToken(req) {
const h = req.headers?.authorization;
if (h && h.startsWith('Bearer ')) return h.slice(7);
@@@ -66,7 +69,10 @@ function requestToken(req) {
function makeRequireAuth(getCtx, signToken) {
return function requireAuth(req, res, next) {
const authHeader = req.headers.authorization;
-- const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : (req.cookies?.ov_token || req.cookies?.ov_sso);
++ // Never ov_sso here: it is SameSite=None, so a hostile page could make the browser send it
++ // on a cross-site POST. Only the read-only /sso/check and the prompt=none authorize
++ // (which yields a code bound to a registered redirect_uri) may consult it.
++ const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : req.cookies?.ov_token;
const ctx = getCtx(req);
const out = verifySession(token, ctx);
if (out.error) return res.status(out.status).json(out.status === 403 ? { error: out.error, ban_reason: out.ban_reason } : { error: out.error });
ddiff --git a/server/auth/sso-check.js b/server/auth/sso-check.js
iindex e796759f..2fbb21d5 100644
---- a/server/auth/sso-check.js
++++ b/server/auth/sso-check.js
@@@ -14,7 +14,9 @@
// Browsers that partition third-party cookies see no ov_sso cookie and get signedIn: false.
// ═══════════════════════════════════════════════════════════════
--const ORIGIN_RE = /^https:\/\/(?:[a-z0-9-]+\.)*(?:openvibe\.[a-z]+|openre\.stream)$/i;
++// Only the zones OpenVibe owns — not openvibe.<anything>, which anyone could register.
++const OWNED = ['openvibe.network', 'openvibe.live', 'openvibe.tools', 'openvibe.media', 'openvibe.games', 'openvibe.community', 'openvibe.chat', 'openvibe.codes', 'openvibe.blog', 'openvibe.wiki', 'openvibe.news', 'openvibe.reviews', 'openvibe.tips', 'openvibe.vip', 'openvibe.trade', 'openvibe.host', 'openvibe.deals', 'openvibe.coupons', 'openre.stream'];
++const ORIGIN_RE = new RegExp(`^https://(?:[a-z0-9-]+\\.)*(?:${OWNED.map(d => d.replace(/\./g, '\\.')).join('|')})$`, 'i');
const LOCAL_RE = /^http:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/;
function allowedOrigin(raw, env = process.env) {
ddiff --git a/test/sso-check.test.js b/test/sso-check.test.js
iindex 751b101f..b51ba629 100644
---- a/test/sso-check.test.js
++++ b/test/sso-check.test.js
@@@ -14,6 +14,8 @@ assert.strictEqual(allowedOrigin('https://play.openvibe.games'), 'https://play.o
assert.strictEqual(allowedOrigin('https://ingest.openre.stream'), 'https://ingest.openre.stream');
assert.strictEqual(allowedOrigin('https://openvibe.tools.evil.com'), null, 'suffix spoof rejected');
assert.strictEqual(allowedOrigin('https://evilopenvibe.tools'), null);
++assert.strictEqual(allowedOrigin('https://openvibe.xyz'), null, 'a TLD we do not own is not ours');
++assert.strictEqual(allowedOrigin('https://x.openvibe.deals'), 'https://x.openvibe.deals');
assert.strictEqual(allowedOrigin('http://openvibe.tools'), null, 'plain http rejected');
assert.strictEqual(allowedOrigin('https://openvibe.tools/path'), null, 'origins only');
assert.strictEqual(allowedOrigin('http://localhost:4301', { NODE_ENV: 'development' }), 'http://localhost:4301');