Merge pull request #813 from PicPeak/feat/harden-picpeak-restore-robustness

feat(security): harden .picpeak restore robustness — sessions, roles, sequences
This commit is contained in:
Paul Nothaft
2026-07-16 13:39:17 +02:00
committed by GitHub
10 changed files with 595 additions and 17 deletions
+19 -1
View File
@@ -2,6 +2,7 @@ const jwt = require('jsonwebtoken');
const { db } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat');
const { isTokenRevoked } = require('../utils/tokenRevocation');
const { isTokenBeforeCutoff } = require('../utils/sessionCutoff');
const logger = require('../utils/logger');
const { getAdminTokenFromRequest, getGalleryTokenFromRequest } = require('../utils/tokenUtils');
@@ -38,6 +39,13 @@ async function adminAuth(req, res, next) {
});
return res.status(401).json({ error: 'Token has been revoked', code: 'TOKEN_REVOKED' });
}
// Reject any session issued before the global cutoff (set by a .picpeak
// restore, which can reassign admin ids). Forces every pre-restore admin
// session to re-authenticate against the restored data.
if (await isTokenBeforeCutoff(decoded)) {
return res.status(401).json({ error: 'Session invalidated', code: 'SESSION_INVALIDATED' });
}
// Verify token type
if (decoded.type !== 'admin') {
@@ -157,7 +165,12 @@ async function galleryAuth(req, res, next) {
if (await isTokenRevoked(decoded)) {
return res.status(401).json({ error: 'Session has been invalidated', code: 'TOKEN_REVOKED' });
}
// Reject sessions issued before the global restore cutoff.
if (await isTokenBeforeCutoff(decoded)) {
return res.status(401).json({ error: 'Session invalidated', code: 'SESSION_INVALIDATED' });
}
// Verify token type
if (decoded.type !== 'gallery') {
return res.status(403).json({ error: 'Invalid access token' });
@@ -221,6 +234,11 @@ async function photoAuth(req, res, next) {
return res.status(401).json({ error: 'Token has been revoked', code: 'TOKEN_REVOKED' });
}
// Reject sessions issued before the global restore cutoff.
if (await isTokenBeforeCutoff(decoded)) {
return res.status(401).json({ error: 'Session invalidated', code: 'SESSION_INVALIDATED' });
}
// Allow both admin and gallery tokens
if (decoded.type === 'admin') {
const admin = await db('admin_users')
+6
View File
@@ -13,6 +13,7 @@ const jwt = require('jsonwebtoken');
const { db } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat');
const { isTokenRevoked } = require('../utils/tokenRevocation');
const { isTokenBeforeCutoff } = require('../utils/sessionCutoff');
const logger = require('../utils/logger');
const { getCustomerTokenFromRequest } = require('../utils/tokenUtils');
@@ -61,6 +62,11 @@ async function customerAuth(req, res, next) {
return res.status(401).json({ error: 'Token has been revoked', code: 'TOKEN_REVOKED' });
}
// Reject sessions issued before the global restore cutoff.
if (await isTokenBeforeCutoff(decoded)) {
return res.status(401).json({ error: 'Session invalidated', code: 'SESSION_INVALIDATED' });
}
if (decoded.type !== 'customer') {
logger.warn('[customerAuth] wrong token type', {
url: req.originalUrl,