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
•Technically reviewed by Anupam Mediratta•Published March 28, 2026•Reviewed 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

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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #792

Related Articles

medium

path-to-regexp 0.1.12 DoS: Catastrophic Backtracking on Malformed URL

path-to-regexp version 0.1.12 contains a regular expression engine vulnerability where maliciously crafted URL parameters trigger exponential backtracking, causing CPU exhaustion and service unavailability. Upgrading to version 0.1.13 eliminates the vulnerable pattern from the parsing logic.

critical

CVE-2026-59873: node-tar 7.5.11 DoS via Crafted Gzip Bomb

node-tar versions 7.5.11 through 7.5.18 are vulnerable to a denial-of-service attack through maliciously crafted gzip archives that decompress to disproportionately large sizes. An attacker can exploit this to exhaust memory and CPU resources by submitting a small, highly compressed archive that expands beyond configured limits during extraction.

high

MapManager.get() Race Condition Duplicates API Requests

The MapManager's `get(mapUid, cache)` method used a check-then-act pattern that permitted multiple concurrent requests to pass the cache miss check simultaneously, triggering redundant API calls and risking cache corruption. The fix introduces a `_pending` promise map to deduplicate in-flight fetches for identical map UIDs.

critical

Lampa Desktop Auto-Update Heuristic Bypass: Execution of Unverified

Lampa Desktop's auto-update mechanism downloaded JavaScript and CSS from `raw.githubusercontent.com` using only heuristic validation—file size thresholds and string pattern matching—that attackers could trivially satisfy. The fix introduces cryptographic integrity verification by cross-referencing Git blob hashes from the GitHub Contents API, ensuring downloaded code matches the repository's authoritative state before execution.

high

adm-zip 0.6.0 Preserves SUID Bits From ZIPs: CVE-2026-102282

The `adm-zip` dependency resolved to 0.6.0 in this project's dependency tree, a version affected by CVE-2026-102282: during extraction it applies the Unix permission bits stored in each ZIP entry's external file attributes verbatim, including the setuid (`04000`), setgid (`02000`), and sticky bits. An attacker who controls an archive passed to `extractAllTo()` or `extractEntryTo()` can therefore have the extractor create a setuid binary owned by whatever user the extraction process runs as. The

high

requestInput() Type Confusion: NaN and Object Bypass in JavaScript

The `requestInput()` utility function lacked validation on its `type` parameter and failed to handle `NaN` results from float conversions, creating a type confusion weakness. An attacker could supply malformed inputs that propagate unhandled `NaN` values or unexpected object types through the type system. The fix adds explicit guards against `NaN` type parameters and rejects non-primitive type values.