Skip to content

[Security][High] Non-expiring download tokens allow persistent file access after access revocation #68

@bmersereau

Description

@bmersereau

Severity: High

File: backend/src/lib/downloadTokens.ts:44-52
CWE: CWE-613 — Insufficient Session Expiration
OWASP: A07:2021 — Identification and Authentication Failures

Description

HMAC-signed download tokens carry no expiration field. Once issued and embedded in chat history, they are permanently valid:

export function signDownload(path: string, filename: string): string {
    const payload = JSON.stringify({ p: path, f: filename });
    // No `exp` field — token never expires

Impact

Users who later lose access to a project or document — through removal from shared_with, document deletion, or account termination — retain the ability to download files via any download URL saved from their chat history. The download route re-checks ownership at request time (providing partial mitigation), but if the signing secret is ever compromised (see #66), all tokens ever issued become immediately forgeable with no expiry to limit the window.

Fix

Add an exp field to the token payload and validate it at verify time:

export function signDownload(path: string, filename: string, ttlSeconds = 900): string {
    const exp = Math.floor(Date.now() / 1000) + ttlSeconds;
    const payload = JSON.stringify({ p: path, f: filename, exp });
    const enc = b64urlEncode(Buffer.from(payload, "utf8"));
    const sig = crypto.createHmac("sha256", getSecret()).update(enc).digest();
    return `${enc}.${b64urlEncode(sig)}`;
}

export function verifyDownload(token: string): { path: string; filename: string } | null {
    // ... existing HMAC check ...
    const parsed = JSON.parse(b64urlDecode(enc).toString("utf8"));
    if (!parsed?.p || !parsed?.f) return null;
    if (parsed.exp && Math.floor(Date.now() / 1000) > parsed.exp) return null;
    return { path: parsed.p, filename: parsed.f };
}

Consider whether download links in chat history should be refreshed on load (client fetches a new signed URL when rendering old messages).

Remediation tier: High — fix before next release.

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