Introduction
Imagine leaving your house keys under the doormat—convenient, but anyone who knows where to look can walk right in. That's essentially what happens when applications store OAuth tokens and API keys in plaintext on the filesystem. This vulnerability, recently patched in the Model Context Protocol (MCP) TypeScript SDK, highlights a critical security oversight that's surprisingly common in modern applications.
For developers, this matters because OAuth tokens are the keys to your users' kingdoms. A compromised token can grant attackers full access to user accounts, sensitive data, and connected services—all without needing a password. Let's dive into what went wrong and how to prevent it.
The Vulnerability Explained
What Was Happening?
The authentication module (plugins/auth-oauth2/src/store.ts) contained two functions—getToken and setToken—that handled OAuth credentials. The problem? These functions were writing tokens directly to the local filesystem without any encryption or obfuscation:
// Vulnerable approach (simplified example)
function setToken(token: string) {
fs.writeFileSync('/path/to/token.json', JSON.stringify({ token }));
}
function getToken() {
const data = fs.readFileSync('/path/to/token.json', 'utf8');
return JSON.parse(data).token;
}
The irony? The project's Rust dependencies (src-tauri/Cargo.lock:3809) already included PBKDF2 (Password-Based Key Derivation Function 2), a robust cryptographic function perfect for this use case. It just wasn't being used.
How Could This Be Exploited?
Attack vectors for plaintext credential storage include:
- Malware Access: Any malicious software running on the user's machine could read these files
- Insider Threats: Users with filesystem access could extract tokens from other accounts
- Backup Exposure: Cloud backups or system snapshots could leak credentials
- Physical Access: Someone with physical access to the device could copy token files
- Privilege Escalation: A low-privilege attacker could escalate by stealing admin tokens
Real-World Impact
Consider this attack scenario:
The Attack:
1. Alice uses an application that stores OAuth tokens in plaintext
2. Bob, a malicious actor, gains limited access to Alice's computer (perhaps through a separate vulnerability or social engineering)
3. Bob locates the token storage file at ~/.config/app/tokens.json
4. Bob copies the file and extracts Alice's OAuth token
5. Bob now has full access to Alice's account on the connected service—no password required
The Consequences:
- Unauthorized access to user data
- Ability to perform actions on behalf of the user
- Potential lateral movement to other connected services
- No audit trail (the legitimate token is being used)
- User remains unaware until suspicious activity is noticed
The Fix
What Changed?
While the specific code changes weren't provided in the PR diff, the fix involves implementing proper encryption for stored credentials. Here's what a secure implementation should look like:
Before (Vulnerable):
// Plaintext storage - INSECURE
export function setToken(token: string): void {
const tokenPath = path.join(CONFIG_DIR, 'token.json');
fs.writeFileSync(tokenPath, JSON.stringify({
token,
timestamp: Date.now()
}));
}
export function getToken(): string | null {
const tokenPath = path.join(CONFIG_DIR, 'token.json');
if (!fs.existsSync(tokenPath)) return null;
const data = JSON.parse(fs.readFileSync(tokenPath, 'utf8'));
return data.token;
}
After (Secure):
import { pbkdf2Sync, randomBytes, createCipheriv, createDecipheriv } from 'crypto';
// Derive encryption key from machine-specific data
function deriveKey(salt: Buffer): Buffer {
const machineId = getMachineIdentifier(); // OS-specific unique ID
return pbkdf2Sync(machineId, salt, 100000, 32, 'sha256');
}
export function setToken(token: string): void {
const tokenPath = path.join(CONFIG_DIR, 'token.enc');
const salt = randomBytes(32);
const iv = randomBytes(16);
const key = deriveKey(salt);
// Encrypt the token
const cipher = createCipheriv('aes-256-gcm', key, iv);
const encrypted = Buffer.concat([
cipher.update(token, 'utf8'),
cipher.final()
]);
const authTag = cipher.getAuthTag();
// Store salt, iv, authTag, and encrypted data
const payload = Buffer.concat([salt, iv, authTag, encrypted]);
fs.writeFileSync(tokenPath, payload);
}
export function getToken(): string | null {
const tokenPath = path.join(CONFIG_DIR, 'token.enc');
if (!fs.existsSync(tokenPath)) return null;
const payload = fs.readFileSync(tokenPath);
// Extract components
const salt = payload.slice(0, 32);
const iv = payload.slice(32, 48);
const authTag = payload.slice(48, 64);
const encrypted = payload.slice(64);
// Derive key and decrypt
const key = deriveKey(salt);
const decipher = createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
try {
const decrypted = Buffer.concat([
decipher.update(encrypted),
decipher.final()
]);
return decrypted.toString('utf8');
} catch (error) {
console.error('Failed to decrypt token');
return null;
}
}
How Does This Solve the Problem?
The secure implementation provides multiple layers of protection:
- PBKDF2 Key Derivation: Creates a strong encryption key from machine-specific data using 100,000 iterations
- AES-256-GCM Encryption: Industry-standard authenticated encryption that prevents tampering
- Random Salt & IV: Ensures each encryption is unique, even for identical tokens
- Authentication Tag: Detects any modifications to the encrypted data
- Machine Binding: The encryption key is derived from machine-specific identifiers, making stolen files useless on other systems
Prevention & Best Practices
How to Avoid This Vulnerability
-
Never Store Secrets in Plaintext
- Always encrypt sensitive data at rest
- Use established cryptographic libraries, don't roll your own crypto -
Use System Keychains When Possible
```typescript
// Better: Use OS-provided secure storage
import keytar from 'keytar';
await keytar.setPassword('myapp', 'oauth-token', token);
const token = await keytar.getPassword('myapp', 'oauth-token');
```
-
Implement Defense in Depth
- Encrypt credentials
- Set restrictive file permissions (0600)
- Store in user-specific directories
- Implement token rotation
- Add expiration times -
Use Environment-Specific Solutions
- Browser: UselocalStoragewith encryption or secure cookies
- Node.js: Use system keychains (Keytar, node-keychain)
- Electron/Tauri: Use safeStorage API
- Mobile: Use Keychain (iOS) or Keystore (Android)
Detection Tools & Techniques
Static Analysis:
- Semgrep: Detect plaintext secret storage patterns
yaml
rules:
- id: plaintext-token-storage
pattern: fs.writeFileSync($PATH, $TOKEN)
message: Potential plaintext credential storage
severity: WARNING
- TruffleHog: Scan commits for accidentally committed secrets
- git-secrets: Prevent secrets from being committed
Code Review Checklist:
- [ ] Are credentials encrypted before storage?
- [ ] Is a strong key derivation function used (PBKDF2, Argon2, scrypt)?
- [ ] Are encryption keys properly protected?
- [ ] Are file permissions restrictive?
- [ ] Is there a token rotation mechanism?
Security Standards & References
- CWE-312: Cleartext Storage of Sensitive Information
- CWE-522: Insufficiently Protected Credentials
- OWASP Top 10 2021: A02:2021 – Cryptographic Failures
- OWASP ASVS: V2.1 Password Security Requirements
- NIST SP 800-132: Recommendation for Password-Based Key Derivation
Conclusion
Storing OAuth tokens and API keys in plaintext is like leaving your front door wide open—it's an invitation for trouble. This vulnerability serves as a crucial reminder that security isn't just about having the right tools (like PBKDF2 in the dependencies), but actually using them correctly.
The key takeaways:
- Always encrypt credentials at rest, even on local filesystems
- Use established cryptographic libraries rather than inventing your own solutions
- Leverage OS-provided secure storage mechanisms when available
- Implement defense in depth with multiple security layers
As developers, we're the guardians of our users' data. Every token, every API key, every credential we handle is a responsibility. By following these best practices and learning from vulnerabilities like this one, we can build more secure applications that protect our users from unnecessary risk.
Remember: convenience should never come at the cost of security. Take the extra time to implement proper encryption—your users will thank you (even if they never know you did it).
Stay secure, and happy coding! 🔒
Have you encountered similar security issues in your projects? Share your experiences and solutions in the comments below.