This guide covers security best practices enforced by the AIDD /review command and built into the AIDD server framework.
TL;DR: Avoid JWT if you can. JWT has historically been riddled with implementation errors.
Use opaque access tokens with server-side sessions or token introspection instead of JWT.
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Client │────▶│ Server │────▶│ Session │
│ │ │ │ │ Store │
│ (cookie) │◀────│ (lookup) │◀────│ (Redis) │
└──────────┘ └──────────┘ └──────────┘
| Storage Method | Security | Recommendation |
|---|---|---|
| httpOnly Secure SameSite cookie | ✅ Safe | Use this |
| localStorage | ❌ XSS vulnerable | Never use |
| sessionStorage | ❌ XSS vulnerable | Never use |
| JavaScript variable | ❌ XSS vulnerable | Never use |
// ❌ CRITICAL: Token in localStorage - XSS vulnerable
localStorage.setItem('token', jwt);
// ❌ CRITICAL: 'none' algorithm - signature bypass
jwt.sign(payload, secret, { algorithm: 'none' });
// ❌ CRITICAL: Verification disabled
jwt.verify(token, secret, { verify: false });
// ❌ CRITICAL: Using decode without verify
const payload = jwt.decode(token); // No signature check!
// ❌ CRITICAL: Ignoring expiration
jwt.verify(token, secret, { ignoreExpiration: true });
// ⚠️ WARNING: Long-lived access tokens
jwt.sign(payload, secret, { expiresIn: '30d' });If you must use JWT, follow these requirements:
- Asymmetric algorithms (RS256/ES256) - never HS256 shared across services
- Short expiration - ≤15 minutes for access tokens
- httpOnly Secure SameSite cookies - never localStorage
- CSRF protection - cookies don't prevent CSRF
- Refresh token rotation - for longer sessions
TL;DR: Don't use timing-safe compare functions. Hash both values with SHA3 and compare the hashes.
Timing attacks exploit the time difference in string comparison to guess secrets character by character. Traditional "fixes" like crypto.timingSafeEqual are insufficient because:
- They still operate on raw values
- Implementation bugs are common
- Raw secrets may leak in logs or errors
Hash both the stored secret and the candidate with SHA3, then compare the hashes:
import { sha3_256 } from 'js-sha3';
const verifyToken = (storedToken, candidateToken) => {
// Hash both values - any bit change fully randomizes the hash
const storedHash = sha3_256(storedToken);
const candidateHash = sha3_256(candidateToken);
// Simple comparison is now safe - no timing oracle
return storedHash === candidateHash;
};- No timing oracle - Hashing removes all prefix structure. Any bit change fully randomizes the hash.
- No raw secrets in memory - The original values are never compared directly.
- No leaks - Raw secrets never appear in logs or error messages.
// ❌ CRITICAL: Raw timing-safe compare still leaks info
crypto.timingSafeEqual(Buffer.from(secret), Buffer.from(candidate));
// ❌ CRITICAL: XOR accumulation tricks
let result = 0;
for (let i = 0; i < secret.length; i++) {
result |= secret[i] ^ candidate[i];
}
return result === 0;
// ❌ CRITICAL: Direct string comparison
return storedToken === candidateToken;TL;DR: Use Cuid2 for all user-visible identifiers. Never use sequential IDs, UUIDs v1-v3, or predictable patterns.
Insecure identifiers have caused real-world breaches:
- Unauthorized account access via GUID prediction - GUIDs are not random enough
- Password reset token prediction - Sequential IDs leaked user enumeration
- GitLab $20,000 bug bounty - Unauthorized data access via predictable IDs
- 2018 Strava Pentagon breach - Location data leaked through predictable patterns
- PleaseRobMe - Social media check-ins revealed when homes were empty
AIDD uses @paralleldrive/cuid2 for secure identifier generation:
import { createId } from '@paralleldrive/cuid2';
const userId = createId(); // 'tz4a98xxat96iws9zmbrgj3a'| Feature | Cuid2 | UUID v4 | Sequential ID |
|---|---|---|---|
| Collision resistant | ✅ ~4e18 IDs to 50% collision | ❌ Requires coordination | |
| Unpredictable | ✅ SHA3 hashed | ❌ Easily guessed | |
| No info leakage | ✅ Hashed output | ❌ Reveals structure | ❌ Reveals count/order |
| Horizontally scalable | ✅ No coordination | ✅ No coordination | ❌ Requires central DB |
| Offline compatible | ✅ Yes | ✅ Yes | ❌ No |
| URL friendly | ✅ Lowercase alphanumeric | ❌ Dashes, long | ✅ Yes |
// ❌ CRITICAL: Sequential IDs - enumerable, predictable
const userId = autoIncrement++; // Attacker can guess all valid IDs
// ❌ CRITICAL: UUID v1 - leaks timestamp and MAC address
import { v1 as uuidv1 } from 'uuid';
const id = uuidv1(); // Contains creation time!
// ⚠️ WARNING: UUID v4 - browser CSPRNG bugs, collisions reported
import { v4 as uuidv4 } from 'uuid';
const id = uuidv4(); // Chrome had Math.random() bugs until 2015
// ⚠️ WARNING: NanoId/Ulid - too fast, trusts single entropy source
import { nanoid } from 'nanoid';
const id = nanoid(); // Fast = easier to brute forceCuid2 uses multiple entropy sources hashed together:
- Current system time
- Pseudorandom values
- Session counter (randomly initialized)
- Host fingerprint
- SHA3 hashing to mix all sources
This provides stronger guarantees than trusting a single source like the Web Crypto API, which has had known bugs.
- Cuid2 Documentation - Full security analysis
- OWASP Identifier Guidelines
The AIDD /review command automatically checks for these security issues. Rules are located in:
ai/rules/security/jwt-security.mdc- JWT patternsai/rules/security/timing-safe-compare.mdc- Secret comparison
To run a security-focused review:
/review
The review command will flag any Critical or Warning patterns found in your code.