fix: change_email and user-protected endpoints (#2680)
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
Notify HeyPuter / notify (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test-backend (24.x) (push) Has been cancelled
test / API tests (node env, api-test) (24.x) (push) Has been cancelled
test / puterjs (node env, vitest) (24.x) (push) Has been cancelled

Update check for suspended users on change_emeail and user-protected
endpoints. The `change_email` endpoint isn't using the auth middleware
because it infers from the token that was passed - this means it needs
explicit logic to check suspended users; before suspended users were
able to complete a a flow for changing their username if they already
started it before being suspended.

Update user-protected endpoints and configurable_auth so endpoints that
make sensitive account changes do not rely on cached information about a
user.
This commit is contained in:
Eric Dubé
2026-03-17 14:54:29 -07:00
committed by GitHub
parent 2bafa03a74
commit ef93ed4572
3 changed files with 12 additions and 4 deletions
@@ -21,7 +21,6 @@ const config = require('../config');
const { LegacyTokenError } = require('../services/auth/AuthService');
const { AccessTokenActorType } = require('../services/auth/Actor');
const { Context } = require('../util/context');
const jwt = require('jsonwebtoken');
// The "/whoami" endpoint is a special case where we want to allow
// a legacy token to be used for authentication. The "/whoami"
@@ -47,6 +46,7 @@ const configurable_auth = options => async (req, res, next) => {
}
const optional = options?.optional;
const allow_cached_user = options?.allow_cached_user;
// Request might already have been authed (PreAuthService)
if ( req.actor ) return next();
@@ -159,6 +159,10 @@ const configurable_auth = options => async (req, res, next) => {
// === Populate Context ===
context.set('actor', actor);
if ( actor.type.user ) {
if ( allow_cached_user === false ) {
const svc_getUser = services.get('get-user');
actor.type.user = await svc_getUser.get_user({ id: actor.type.user.id, force: true });
}
if ( actor.type.user?.suspended ) {
throw APIError.create('forbidden');
}
+5 -1
View File
@@ -44,13 +44,17 @@ const CHANGE_EMAIL_CONFIRM = eggspress('/change_email/confirm', {
const db = req.services.get('database').get(DB_WRITE, 'auth');
const rows = await db.read(
'SELECT `unconfirmed_change_email` FROM `user` WHERE `id` = ? AND `change_email_confirm_token` = ?',
'SELECT `unconfirmed_change_email`, `suspended` FROM `user` WHERE `id` = ? AND `change_email_confirm_token` = ?',
[user_id, token],
);
if ( rows.length === 0 ) {
throw APIError.create('token_invalid');
}
if ( rows[0].suspended ) {
throw APIError.create('forbidden');
}
const svc_cleanEmail = req.services.get('clean-email');
const clean_email = svc_cleanEmail.clean(rows[0].unconfirmed_change_email);
@@ -84,8 +84,8 @@ class UserProtectedEndpointsService extends BaseService {
next();
});
// Require authenticated session
router.use(configurable_auth({ no_options_auth: true }));
// Require authenticated session; bypass user cache to enforce suspension reliably
router.use(configurable_auth({ no_options_auth: true, allow_cached_user: false }));
// Only allow user sessions with HTTP powers (session token), not GUI tokens or API tokens
router.use((req, res, next) => {