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 Orbis AppSec
Published March 28, 2026Reviewed June 3, 2026

Answer Summary

Plaintext credential storage (CWE-256) occurs when authentication systems write sensitive tokens like OAuth credentials or API keys directly to disk without encryption. In this case, the authentication module stored credentials in plaintext despite having PBKDF2 available. The fix encrypts all credentials before writing them to the filesystem using PBKDF2-derived keys, ensuring that even if an attacker gains filesystem access, the credentials remain protected.

Vulnerability at a Glance

cweCWE-256
fixEncrypt credentials using PBKDF2-derived keys before storage
riskUnauthorized access to OAuth tokens and API keys
languageAuthentication Module (Language-agnostic pattern)
root causeCredentials written directly to filesystem without encryption
vulnerabilityPlaintext Storage of Credentials

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.

Frequently Asked Questions

What is plaintext credential storage?

Plaintext credential storage occurs when sensitive authentication data like passwords, tokens, or API keys are saved to disk or databases without encryption, making them readable by anyone with access to the storage medium.

How do you prevent plaintext credential storage in authentication systems?

Use strong encryption (AES-256) with keys derived from secure key derivation functions like PBKDF2, bcrypt, or Argon2. Store the encrypted data and never write raw credentials to disk.

What CWE is plaintext credential storage?

CWE-256 (Plaintext Storage of a Password) and the related CWE-312 (Cleartext Storage of Sensitive Information) cover this vulnerability class.

Is file permission restriction enough to prevent credential theft?

No, file permissions alone are insufficient. Malware, privilege escalation attacks, backup exposure, and insider threats can bypass permissions. Encryption provides defense-in-depth.

Can static analysis detect plaintext credential storage?

Yes, static analysis tools can detect patterns where sensitive data flows to file write operations without passing through encryption functions, though they may require configuration to identify credential variables.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #792

Related Articles

high

How missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration vulnerability was discovered in a `.github/dependabot.yml` file that lacked a cooldown period for package updates. Without this safeguard, Dependabot could immediately propose updates to newly published package versions—including potentially malicious or unstable releases. The fix adds a simple `cooldown` block with a 7-day waiting period before any new package version is suggested.

high

How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it

A high-severity Server-Sent Events (SSE) injection vulnerability (CVE-2026-33128) was discovered in the h3 HTTP framework, where unsanitized newline characters in event stream fields could allow attackers to inject arbitrary SSE messages. The fix upgrades h3 from version 1.15.5 to 1.15.6 in the frontend's dependency tree, ensuring that newline characters are properly sanitized before being written to event streams.

high

How Memory Exhaustion via Large Comma-Separated Selector Lists happens in Python Soup Sieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in Soup Sieve version 2.8.3, affecting Python applications that parse CSS selectors from user-controlled input. The vulnerability allows attackers to craft malicious selector lists that consume excessive memory, potentially causing denial of service. The fix involves upgrading to soupsieve 2.8.4, which implements proper resource limits on selector parsing.

high

How prototype pollution via `__proto__` key happens in Node.js defu and how to fix it

A high-severity prototype pollution vulnerability (CVE-2026-35209) was discovered in the `defu` package version 6.1.4, which allowed attackers to inject properties into JavaScript's `Object.prototype` via the `__proto__` key in defaults arguments. The fix upgrades `defu` to version 6.1.5 in the frontend's dependency tree, protecting downstream consumers like `c12` and `dotenv` configuration loaders from malicious property injection.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.

high

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.