openvibers/openvibe.blog Commits

Open redirect fixed (roadmap WS-R task 5): the sign-in next let "/<TAB>/evil.com" through, which browsers read as "//evil.com" (they drop tab and newline characters and read a backslash as "/"); any control character or backslash in next now goes home. test/open-redirect.test.js feeds it the known bypasses

OP OpenVibers committed 26 Sept 2026 · 047c9562ce · 2 files, +23 −0


Changes

ddiff --git a/server/auth/sso.js b/server/auth/sso.js
iindex f2d8783..e713992 100644
---- a/server/auth/sso.js
++++ b/server/auth/sso.js
@@@ -136,6 +136,9 @@ function fedcmNonceMatches(token, nonce) {
* https://openvibe.network/… URL (sign-in / sign-out-everywhere chains) as post-auth targets.
*/
function sanitizeNext(next, config) {
++ // Browsers drop tab and newline characters from a URL and read a backslash as "/": "/<TAB>/evil.com" would
++ // leave the site. A next with any control character or backslash goes home.
++ if (typeof next === 'string' && /[\u0000-\u001f\u007f\\]/.test(next)) return '/';
if (!next || typeof next !== 'string') return '/';
if (/^\/(?!\/|\\)/.test(next)) return next; // relative path, not protocol-relative
try {
ddiff --git a/test/open-redirect.test.js b/test/open-redirect.test.js
nnew file mode 100644
iindex 0000000..175c4fe
---- /dev/null
++++ b/test/open-redirect.test.js
@@@ -0,0 +1,20 @@
++'use strict';
++// Open redirect regression (WS-R task 5): the sign-in "next" never leaves the site. Browsers drop tab and
++// newline characters from a URL and read a backslash as "/", so "/<TAB>/evil.com" is "//evil.com" to them;
++// until 2026-09-26 it got through here.
++const assert = require('assert');
++const { sanitizeNext } = require('../server/auth/sso');
++
++const config = { baseUrl: 'https://site.openvibe.test', networkUrl: 'https://openvibe.network' };
++const home = (v) => {
++ const out = String(sanitizeNext(v, config));
++ const host = new URL(out.replace(/[\t\n\r]/g, ''), 'https://site.openvibe.test').host;
++ return host === 'site.openvibe.test' || host === 'openvibe.network';
++};
++for (const v of ['//evil.com', '/\\evil.com', '\\\\evil.com', 'https://evil.com', 'http:evil.com', '/\t/evil.com', '/\n/evil.com', '\t//evil.com',
++ ' //evil.com', '/\\/evil.com', 'https:/\\evil.com', '///evil.com', 'javascript:alert(1)', '/%09/evil.com']) {
++ assert.ok(home(v), `${JSON.stringify(v)} stays on the site (got ${JSON.stringify(sanitizeNext(v, config))})`);
++ assert.ok(!/^\s*javascript:/i.test(String(sanitizeNext(v, config))), JSON.stringify(v));
++}
++assert.strictEqual(sanitizeNext('/settings?tab=a', config), '/settings?tab=a', 'a plain relative path is kept');
++console.log('open redirect: all checks passed');