Skip to content

[Security] Download token HMAC falls back to hardcoded "dev-secret" when env vars are absent #7

@robinjfisher

Description

@robinjfisher

Summary

Download tokens are signed with an HMAC key resolved in backend/src/lib/downloadTokens.ts. If neither DOWNLOAD_SIGNING_SECRET nor SUPABASE_SECRET_KEY is set, the signing key falls back to the literal string "dev-secret":

function getSecret(): string {                                                                                                                                     
    return (    
        process.env.DOWNLOAD_SIGNING_SECRET ??
        process.env.SUPABASE_SECRET_KEY ??                                                                                                                         
        "dev-secret"
    );                                                                                                                                                             
}               

Because this codebase is public, the fallback value is known to anyone. An attacker who determines that a deployment is running without these env vars can compute valid HMAC signatures for any storage path they can guess or enumerate, producing forged download tokens that the /download/:token route will accept.

Impact

  • Any authenticated user (or unauthenticated user if the route were ever relaxed) can forge a download token for an arbitrary R2 storage path without possessing the real secret.
  • The /download/:token route still requires a valid Supabase session, so this does not enable fully unauthenticated access — but it does break the token's purpose of scoping downloads to authorised files.
  • The companion risk: DOWNLOAD_SIGNING_SECRET is not listed in .env.example, so a deployer following the README will not know to set it, and the fallback to SUPABASE_SECRET_KEY (if set) silently doubles that key's role without documentation.

Steps to reproduce

  1. Deploy the backend without setting DOWNLOAD_SIGNING_SECRET or SUPABASE_SECRET_KEY.
  2. Compute HMAC-SHA256("dev-secret", ) for a payload {"p":"","f":""}.
  3. Issue a GET /download/ with a valid Supabase session.
  4. Observe the file is served without the token having been issued by the server.

Proposed fix

  1. Remove the "dev-secret" fallback entirely. If neither env var is set, fail fast at startup rather than silently degrading:
function getSecret(): string {                                                                                                                                     
    const secret =
        process.env.DOWNLOAD_SIGNING_SECRET ??
        process.env.SUPABASE_SECRET_KEY;                                                                                                                           
    if (!secret) throw new Error(
        "DOWNLOAD_SIGNING_SECRET must be set"                                                                                                                      
    );          
    return secret;
}                            
  1. Add DOWNLOAD_SIGNING_SECRET to backend/.env.example with a clear comment explaining it should be a randomly generated secret distinct from the Supabase key.
  2. Ideally, use a dedicated secret rather than reusing SUPABASE_SECRET_KEY, which already carries significant privilege.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions