fix(security): share-login must not bypass gallery password (GHSA-9hmx-68vc-qpqw)
POST /auth/gallery/share-login validated only the 128-bit share token and then
minted a full type:'gallery' access token regardless of require_password —
computing requiresPassword at the end only to echo it, never enforce it. Anyone
holding a gallery's share link could read and download every photo in a
password-protected gallery via a direct API call, no password needed.
Fix: compute requiresPassword before minting; for a password-protected gallery
return { requires_password: true } with NO token and NO cookie. The client then
goes through /gallery/verify, which does bcrypt.compare the password. The public
(no-password) auto-login path is unchanged. The frontend already falls through
to the password prompt when share-login returns no token/event.
Adds route regression test covering the bypass, the public path, and bad tokens.
This commit is contained in:
@@ -554,6 +554,18 @@ router.post('/gallery/share-login', [
|
||||
return res.status(401).json({ error: 'Invalid or expired share link' });
|
||||
}
|
||||
|
||||
const requiresPassword = !(event.require_password === false || event.require_password === 0 || event.require_password === '0');
|
||||
|
||||
// The share link only proves the holder was given the link — it is NOT the
|
||||
// gallery password. For a password-protected gallery, minting a full
|
||||
// `type:'gallery'` token here would let anyone with the share URL bypass
|
||||
// the password entirely (GHSA-9hmx-68vc-qpqw). Signal that a password is
|
||||
// still required and return WITHOUT a token/cookie; the client then goes
|
||||
// through POST /gallery/verify, which does check the password.
|
||||
if (requiresPassword) {
|
||||
return res.json({ requires_password: true });
|
||||
}
|
||||
|
||||
const jwtToken = jwt.sign({
|
||||
eventId: event.id,
|
||||
eventSlug: event.slug,
|
||||
@@ -568,8 +580,6 @@ router.post('/gallery/share-login', [
|
||||
await trackSuccessfulLogin(`gallery:${event.slug}:share`, ipAddress, userAgent);
|
||||
setGalleryAuthCookies(res, jwtToken, event.slug);
|
||||
|
||||
const requiresPassword = !(event.require_password === false || event.require_password === 0 || event.require_password === '0');
|
||||
|
||||
res.json({
|
||||
token: jwtToken,
|
||||
event: {
|
||||
|
||||
Reference in New Issue
Block a user