Auth/access-control audit fixes (all pre-existing on main; none are regressions). Verified end-to-end where noted. HIGH - Thumbnail enumeration: photoAuth granted any gallery token access to any flat /thumbnails/thumb_* file, so a visitor to one gallery could enumerate another (password-protected) gallery's entire thumbnail set. Scope thumbnail access to the token's event via photos.thumbnail_path. Live-verified: cross-event fetch now 404s, own-event still 200s. - Bulk ownership bypass: bulk-archive/bulk-delete acted on body-supplied event ids with no owner filter (single-event routes enforce requireEventOwnership), letting admin/editor archive or cascade-delete any event. Add filterOwnedEventIds; also guard rename + import-external; tighten photo-retry to scope admin (not just editor). Fix misleading bulk-delete comment. MED - verifyGalleryAccess never checked decoded.type — assert 'gallery' instead of relying on other token types incidentally lacking eventId. - secure-images generate-token/secure-download missing denySlideshowToken (#646 bypass): a leaked slideshow token could download originals. - Frontend: AuthenticatedImage + api.ts attached the gallery bearer token to absolute/external URLs — only attach to relative same-app paths. LOW hardening - Pin algorithms:['HS256'] on all auth-boundary jwt.verify calls. - crypto.timingSafeEqual for share-token + HMAC compares (utils/timingSafe). - Remove dead photoAuth import in galleryFeedback. Tests: new regression suites for thumbnail scoping + filterOwnedEventIds; fixed verifyGalleryAccess.customerRevoke fixture (real customer tokens carry type:'gallery'). Full backend suite at the pre-existing baseline (5 suites/27 tests fail on main too), zero new failures.
107 lines
2.7 KiB
JavaScript
107 lines
2.7 KiB
JavaScript
const jwt = require('jsonwebtoken');
|
|
const { db } = require('../database/db');
|
|
const logger = require('../utils/logger');
|
|
const { getGuestTokenFromRequest } = require('../utils/tokenUtils');
|
|
|
|
/**
|
|
* Non-blocking middleware. Reads an optional guest token from the request and,
|
|
* if present and valid, populates req.guest with { id, identifier, name, eventId }.
|
|
*
|
|
* If the token is missing, malformed, or expired → req.guest = null and the
|
|
* request continues. Downstream handlers (e.g. feedback submission) enforce
|
|
* presence explicitly based on event feedback settings (identity_mode).
|
|
*/
|
|
async function resolveGuest(req, res, next) {
|
|
try {
|
|
const slug = req.params?.slug;
|
|
const token = getGuestTokenFromRequest(req, slug);
|
|
if (!token) {
|
|
req.guest = null;
|
|
return next();
|
|
}
|
|
|
|
let decoded;
|
|
try {
|
|
const verified = jwt.verify(token, process.env.JWT_SECRET, {
|
|
algorithms: ['HS256'],
|
|
issuer: 'picpeak-auth',
|
|
complete: true,
|
|
});
|
|
decoded = verified.payload;
|
|
} catch (err) {
|
|
// Invalid or expired guest tokens are silently ignored so that public
|
|
// gallery browsing continues to work even if the token is stale.
|
|
logger.debug('Invalid guest token', { reason: err.message });
|
|
req.guest = null;
|
|
return next();
|
|
}
|
|
|
|
if (decoded.type !== 'guest') {
|
|
req.guest = null;
|
|
return next();
|
|
}
|
|
|
|
// Verify the guest row still exists and is not soft-deleted.
|
|
const guest = await db('gallery_guests')
|
|
.where({ id: decoded.guestId, event_id: decoded.eventId, is_deleted: false })
|
|
.first();
|
|
|
|
if (!guest) {
|
|
req.guest = null;
|
|
return next();
|
|
}
|
|
|
|
req.guest = {
|
|
id: guest.id,
|
|
eventId: guest.event_id,
|
|
identifier: guest.identifier,
|
|
name: guest.name,
|
|
email: guest.email || null,
|
|
};
|
|
|
|
return next();
|
|
} catch (error) {
|
|
logger.error('resolveGuest middleware error', { error: error.message });
|
|
req.guest = null;
|
|
return next();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Blocking middleware that 401s if no guest identity was resolved.
|
|
* Use this on endpoints that require a valid guest session.
|
|
*/
|
|
function requireGuest(req, res, next) {
|
|
if (!req.guest) {
|
|
return res.status(401).json({ error: 'Guest identity required' });
|
|
}
|
|
return next();
|
|
}
|
|
|
|
/**
|
|
* Sign a new guest JWT. Scoped to a specific event and guest row.
|
|
* Expiry matches the gallery token default (24h).
|
|
*/
|
|
function signGuestToken({ guestId, eventId, identifier, name }, expiresIn = '24h') {
|
|
return jwt.sign(
|
|
{
|
|
type: 'guest',
|
|
guestId,
|
|
eventId,
|
|
identifier,
|
|
name,
|
|
},
|
|
process.env.JWT_SECRET,
|
|
{
|
|
issuer: 'picpeak-auth',
|
|
expiresIn,
|
|
}
|
|
);
|
|
}
|
|
|
|
module.exports = {
|
|
resolveGuest,
|
|
requireGuest,
|
|
signGuestToken,
|
|
};
|