Summary
The POST /api/auth/upload-avatar and DELETE /api/auth/upload-avatar endpoints decode the JWT payload using atob() without validating the cryptographic signature. This allows any client to craft a JWT with an arbitrary sub claim and upload/delete avatars for other users.
Location
src/app/api/auth/upload-avatar/route.js — lines 13-42 (POST) and lines 130-159 (DELETE)
Root Cause
The code manually splits the JWT into parts and decodes the payload:
const tokenParts = token.split('.');
const payload = JSON.parse(atob(tokenParts[1]));
user = { id: payload.sub, email: payload.email };
It checks token format (3 parts) and expiration, but never validates the signature (the third part). An attacker can set sub to any user ID.
The endpoint then uses a service role client (SUPABASE_SERVICE_ROLE_KEY) to update the database, bypassing RLS entirely.
Impact
- An attacker can upload a malicious avatar for any user
- An attacker can delete any user's avatar (via DELETE)
- Storage pollution under arbitrary user ID paths
Reproduction
// Craft a JWT with arbitrary sub — no valid signature needed
const fakePayload = btoa(JSON.stringify({
sub: 'TARGET_USER_AUTH_ID',
email: 'attacker@example.com',
exp: Math.floor(Date.now() / 1000) + 3600
}));
const fakeToken = `eyJhbGciOiJIUzI1NiJ9.${fakePayload}.fakesignature`;
// Server accepts this token and updates TARGET_USER's avatar
fetch('/api/auth/upload-avatar', {
method: 'POST',
headers: { 'Authorization': `Bearer ${fakeToken}` },
body: formDataWithImage
});
Fix
Replace the manual JWT decoding with supabase.auth.getUser() which validates the signature via Supabase's auth infrastructure. Other endpoints in this codebase (e.g., /api/crypto/public-keys) already use this pattern correctly.
Comparison
The correct pattern exists at /api/crypto/public-keys/route.js:
const { data: { user }, error } = await supabase.auth.getUser(accessToken);
Summary
The
POST /api/auth/upload-avatarandDELETE /api/auth/upload-avatarendpoints decode the JWT payload usingatob()without validating the cryptographic signature. This allows any client to craft a JWT with an arbitrarysubclaim and upload/delete avatars for other users.Location
src/app/api/auth/upload-avatar/route.js— lines 13-42 (POST) and lines 130-159 (DELETE)Root Cause
The code manually splits the JWT into parts and decodes the payload:
It checks token format (3 parts) and expiration, but never validates the signature (the third part). An attacker can set
subto any user ID.The endpoint then uses a service role client (
SUPABASE_SERVICE_ROLE_KEY) to update the database, bypassing RLS entirely.Impact
Reproduction
Fix
Replace the manual JWT decoding with
supabase.auth.getUser()which validates the signature via Supabase's auth infrastructure. Other endpoints in this codebase (e.g.,/api/crypto/public-keys) already use this pattern correctly.Comparison
The correct pattern exists at
/api/crypto/public-keys/route.js: