Back to Blog
high SEVERITY6 min read

OAuth Tokens Exposed: Why Plaintext Credential Storage Is a Critical Mistake

A medium-severity vulnerability was discovered where OAuth tokens and API keys were being stored in plaintext on the local filesystem without any encryption. Despite having PBKDF2 cryptographic capabilities available in the project dependencies, the authentication module was writing sensitive credentials directly to disk, leaving them vulnerable to unauthorized access. This fix addresses a common but dangerous security oversight that could compromise user accounts and API access.

O
By orbisai0security
March 28, 2026
#security#oauth#encryption#credentials#authentication#pbkdf2#vulnerability

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:

  1. Malware Access: Any malicious software running on the user's machine could read these files
  2. Insider Threats: Users with filesystem access could extract tokens from other accounts
  3. Backup Exposure: Cloud backups or system snapshots could leak credentials
  4. Physical Access: Someone with physical access to the device could copy token files
  5. 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:

  1. PBKDF2 Key Derivation: Creates a strong encryption key from machine-specific data using 100,000 iterations
  2. AES-256-GCM Encryption: Industry-standard authenticated encryption that prevents tampering
  3. Random Salt & IV: Ensures each encryption is unique, even for identical tokens
  4. Authentication Tag: Detects any modifications to the encrypted data
  5. 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

  1. Never Store Secrets in Plaintext
    - Always encrypt sensitive data at rest
    - Use established cryptographic libraries, don't roll your own crypto

  2. 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');
```

  1. Implement Defense in Depth
    - Encrypt credentials
    - Set restrictive file permissions (0600)
    - Store in user-specific directories
    - Implement token rotation
    - Add expiration times

  2. Use Environment-Specific Solutions
    - Browser: Use localStorage with 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.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #792

Related Articles

high

How Missing Checksum Validation Opens the Door to Supply Chain Attacks

A high-severity vulnerability was discovered in a web application's file download pipeline where the `nodejs-file-downloader` dependency was used without any cryptographic verification of downloaded content. Without checksum or signature validation, attackers positioned between the server and client could silently swap legitimate files for malicious ones. This fix closes that window by enforcing integrity verification before any downloaded content is trusted or executed.

high

Unauthenticated Debug Endpoints Expose Firmware Internals: A High-Severity Fix

A high-severity vulnerability was discovered and patched in firmware package handling code, where debug and monitoring endpoints were left exposed without any authentication, authorization, or IP restrictions. These endpoints leaked sensitive application internals including thread states, database connection pool statistics, and potentially sensitive data stored in thread-local storage. Left unpatched, this flaw could allow any unauthenticated attacker to map out application internals and pivot

high

Heap Buffer Overflow in SSL/TLS: When Proto Length Goes Wrong

A critical heap buffer overflow vulnerability was discovered and patched in `src/ssl.c`, where improper bounds checking during ALPN/NPN protocol list construction could allow an attacker to corrupt heap memory and potentially execute arbitrary code. The fix addresses both the missing capacity validation and a dangerous integer overflow in size arithmetic that could lead to undersized allocations followed by out-of-bounds writes. Understanding this class of vulnerability is essential for any deve