Compare commits

...

2 commits

Author SHA1 Message Date
Julian Lam
a5c7d864c6 7.6.0 2025-09-18 13:32:35 -04:00
Julian Lam
1b32abbf11 fix: open redirection vulnerability 2025-09-18 13:32:28 -04:00
5 changed files with 23 additions and 10 deletions

View file

@ -16,6 +16,13 @@ const user = require.main.require('./src/user');
const meta = require.main.require('./src/meta');
const helpers = require.main.require('./src/controllers/helpers');

const guard = (path) => {
let url = new URL(path, nconf.get('url'));
url = url.hostname === nconf.get('url_parsed').hostname ? url : nconf.get('url');

return url.toString();
};

const Controllers = module.exports;


@ -70,7 +77,7 @@ Controllers.renderTotpChallenge = async (req, res, next) => {
const single = parseInt(req.query.single, 10) === 1;

if (req.session.tfa === true && !req.session.tfaForce) {
return res.redirect(nconf.get('relative_path') + (req.query.next || '/'));
return res.redirect(guard(nconf.get('relative_path') + (req.query.next || '/')));
}

const error = req.flash('error');
@ -97,7 +104,7 @@ Controllers.renderAuthnChallenge = async (req, res, next) => {
const single = parseInt(req.query.single, 10) === 1;

if (req.session.tfa === true && ((req.query.next && !req.query.next.startsWith('/admin')) || !req.session.tfaForce)) {
return res.redirect(nconf.get('relative_path') + (req.query.next || '/'));
return res.redirect(guard(nconf.get('relative_path') + (req.query.next || '/')));
}

if (!await parent.hasAuthn(uid)) {
@ -137,7 +144,7 @@ Controllers.renderBackup = async (req, res, next) => {
const single = parseInt(req.query.single, 10) === 1;

if (req.session.tfa === true && ((req.query.next && !req.query.next.startsWith('/admin')) || !req.session.tfaForce)) {
return res.redirect(nconf.get('relative_path') + (req.query.next || '/'));
return res.redirect(guard(nconf.get('relative_path') + (req.query.next || '/')));
}

const error = req.flash('error');

View file

@ -21,6 +21,12 @@ const controllerHelpers = require.main.require('./src/controllers/helpers');
const SocketPlugins = require.main.require('./src/socket.io/plugins');

const atob = base64str => Buffer.from(base64str, 'base64').toString('binary');
const guard = (path) => {
let url = new URL(path, nconf.get('url'));
url = url.hostname === nconf.get('url_parsed').hostname ? url : nconf.get('url');

return url.toString();
};

const plugin = {
_f2l: undefined,
@ -57,7 +63,7 @@ plugin.init = async (params) => {
delete req.session.tfaForce;
req.session.meta.datetime = Date.now();
user.auth.addSession(req.uid, req.sessionID, req.session.meta.uuid);
res.redirect(nconf.get('relative_path') + (req.query.next || '/'));
res.redirect(guard(nconf.get('relative_path') + (req.query.next || '/')));
});
hostHelpers.setupPageRoute(router, '/login/2fa/authn', [hostMiddleware.ensureLoggedIn], controllers.renderAuthnChallenge);

@ -65,7 +71,7 @@ plugin.init = async (params) => {
hostHelpers.setupPageRoute(router, '/login/2fa/backup', [hostMiddleware.ensureLoggedIn], controllers.renderBackup);
router.post('/login/2fa/backup', hostMiddleware.ensureLoggedIn, controllers.processBackup, (req, res) => {
req.session.tfa = true;
res.redirect(nconf.get('relative_path') + (req.query.next || '/'));
res.redirect(guard(nconf.get('relative_path') + (req.query.next || '/')));
});
router.put('/login/2fa/backup', hostMiddleware.requireUser, middlewares.requireSecondFactor, hostMiddleware.applyCSRF, controllers.generateBackupCodes);

@ -167,7 +173,7 @@ plugin.addRoutes = async ({ router, middleware, helpers }) => {
req.session.meta.datetime = Date.now();

helpers.formatApiResponse(200, res, {
next: req.query.next || '/',
next: guard(req.query.next || '/'),
});
});


4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "nodebb-plugin-2factor",
"version": "7.5.10",
"version": "7.6.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "nodebb-plugin-2factor",
"version": "7.5.10",
"version": "7.6.0",
"license": "MIT",
"dependencies": {
"@github/webauthn-json": "^0.5.7",

View file

@ -1,6 +1,6 @@
{
"name": "nodebb-plugin-2factor",
"version": "7.5.10",
"version": "7.6.0",
"description": "",
"main": "library.js",
"repository": {

View file

@ -22,7 +22,7 @@ define('forum/login-authn', ['api', 'alerts', 'hooks'], function (api, alerts, h
iconEl.classList.remove('fa-spin');
iconEl.classList.add('fa-check');
iconEl.classList.add('text-success');
document.location = config.relative_path + next;
document.location = next;
}).catch((err) => {
alerts.error(err);
ajaxify.refresh();