Back to Blog
high SEVERITY6 min read

Plaintext OAuth Token Storage: A Silent Security Risk in Your Application

A medium-severity vulnerability was discovered where OAuth tokens and API keys were stored in plaintext on the local filesystem without encryption. Despite having PBKDF2 cryptographic capabilities available in the application's dependencies, these sensitive credentials were written directly to disk, exposing users to potential credential theft and unauthorized account access.

O
By Orbis AppSec
Published March 28, 2026Reviewed June 3, 2026

Answer Summary

This vulnerability is a plaintext credential storage issue (CWE-312) where OAuth tokens and API keys were written directly to disk without encryption, despite PBKDF2 cryptographic utilities being available in the application's dependency tree. An attacker with local filesystem read access — through malware, a compromised process, or physical access — could steal these credentials and gain full unauthorized access to any connected accounts. The fix encrypts credentials at rest using PBKDF2 key derivation before writing them to disk, and decrypts them transparently on read. Developers should audit any file-based token caching code to ensure sensitive values are never stored in plaintext.

Vulnerability at a Glance

cweCWE-312
fixEncrypt credentials using PBKDF2 key derivation before writing to disk; decrypt transparently on read
riskCredential theft enabling full account takeover for any user whose token file is readable
languageN/A (filesystem credential caching pattern)
root causeSensitive credentials written directly to disk without invoking available PBKDF2 encryption utilities
vulnerabilityPlaintext OAuth Token and API Key Storage

Introduction

Authentication credentials are the keys to your digital kingdom. When OAuth tokens and API keys are stored insecurely, you're essentially leaving those keys under the doormat. This vulnerability, recently patched in a popular application's authentication module, highlights a critical but often overlooked security concern: how we store sensitive credentials matters just as much as how we transmit them.

Developers frequently focus on securing credentials in transit using HTTPS and TLS, but forget that credentials at rest require equal protection. This oversight can turn a user's local machine into a treasure trove for attackers, malware, or even curious insiders.

The Vulnerability Explained

What Was Happening?

The vulnerability existed in the OAuth2 authentication plugin, specifically in the store.ts file's getToken and setToken functions. These functions were responsible for persisting OAuth tokens and API keys to the local filesystem—but they were doing so in plaintext.

Here's what the vulnerable code pattern looked like:

// Vulnerable code pattern (simplified)
function setToken(token: string, apiKey: string) {
  // Writing credentials directly to filesystem
  fs.writeFileSync('credentials.json', JSON.stringify({
    oauth_token: token,
    api_key: apiKey
  }));
}

function getToken() {
  // Reading plaintext credentials
  const data = fs.readFileSync('credentials.json', 'utf-8');
  return JSON.parse(data);
}

The Real Problem

The irony? The application already had PBKDF2 (Password-Based Key Derivation Function 2) available in its Rust dependencies at src-tauri/Cargo.lock:3809. This cryptographic function was sitting unused while sensitive credentials were being written to disk in a format anyone could read.

How Could This Be Exploited?

Let's walk through a realistic attack scenario:

Scenario: The Malicious Browser Extension

  1. Initial Access: A user installs a seemingly innocent browser extension or downloads malware disguised as legitimate software
  2. File System Scanning: The malicious code scans common application data directories for configuration files
  3. Credential Harvesting: It discovers the plaintext credentials file and exfiltrates the OAuth tokens and API keys
  4. Account Takeover: The attacker uses these tokens to:
    - Access the victim's account without needing their password
    - Make API calls on behalf of the user
    - Access sensitive data or resources
    - Potentially pivot to other connected services

Additional Attack Vectors:

  • Physical Access: Someone with temporary physical access to an unlocked computer could copy credential files
  • Backup Exposure: Plaintext credentials in cloud-synced folders or backups become accessible to anyone who compromises those services
  • Insider Threats: System administrators or other users with file system access could harvest credentials
  • Ransomware Exfiltration: Modern ransomware often exfiltrates data before encryption—plaintext credentials are prime targets

Real-World Impact

The severity rating of "medium" might seem modest, but the impact can be severe:

  • Immediate Account Compromise: No password cracking needed—tokens provide direct access
  • Lateral Movement: OAuth tokens often grant access to multiple services and APIs
  • Long-lived Tokens: Many OAuth implementations use long-lived refresh tokens, extending the window of vulnerability
  • Compliance Violations: Plaintext credential storage violates PCI DSS, GDPR, SOC 2, and other security standards

The Fix

What Changed?

The fix involved implementing proper encryption for stored credentials using the already-available PBKDF2 cryptographic function. Here's what a secure implementation looks like:

// Secure implementation (example)
import { pbkdf2Sync, randomBytes, createCipheriv, createDecipheriv } from 'crypto';

const ALGORITHM = 'aes-256-gcm';
const SALT_LENGTH = 32;
const KEY_LENGTH = 32;
const IV_LENGTH = 16;
const TAG_LENGTH = 16;

function deriveKey(password: string, salt: Buffer): Buffer {
  // Use PBKDF2 to derive encryption key
  return pbkdf2Sync(password, salt, 100000, KEY_LENGTH, 'sha256');
}

function setToken(token: string, apiKey: string, masterPassword: string) {
  const salt = randomBytes(SALT_LENGTH);
  const iv = randomBytes(IV_LENGTH);
  const key = deriveKey(masterPassword, salt);

  const cipher = createCipheriv(ALGORITHM, key, iv);

  const credentials = JSON.stringify({
    oauth_token: token,
    api_key: apiKey
  });

  let encrypted = cipher.update(credentials, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  const authTag = cipher.getAuthTag();

  // Store salt, IV, auth tag, and encrypted data
  const secureData = {
    salt: salt.toString('hex'),
    iv: iv.toString('hex'),
    authTag: authTag.toString('hex'),
    encrypted: encrypted
  };

  fs.writeFileSync('credentials.enc', JSON.stringify(secureData));
}

function getToken(masterPassword: string) {
  const data = JSON.parse(fs.readFileSync('credentials.enc', 'utf-8'));

  const salt = Buffer.from(data.salt, 'hex');
  const iv = Buffer.from(data.iv, 'hex');
  const authTag = Buffer.from(data.authTag, 'hex');
  const key = deriveKey(masterPassword, salt);

  const decipher = createDecipheriv(ALGORITHM, key, iv);
  decipher.setAuthTag(authTag);

  let decrypted = decipher.update(data.encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');

  return JSON.parse(decrypted);
}

Security Improvements

The fix provides multiple layers of security:

  1. PBKDF2 Key Derivation: Transforms a master password into a strong encryption key using 100,000 iterations, making brute-force attacks computationally expensive

  2. AES-256-GCM Encryption: Uses authenticated encryption to protect both confidentiality and integrity of stored credentials

  3. Unique Salts and IVs: Each encryption operation uses random salt and initialization vectors, preventing rainbow table attacks

  4. Authentication Tags: GCM mode provides built-in authentication, detecting any tampering with encrypted data

Prevention & Best Practices

Never Store Credentials in Plaintext

This should be a fundamental rule, but it's worth emphasizing: sensitive data at rest must be encrypted. This applies to:

  • OAuth tokens and refresh tokens
  • API keys and secrets
  • Session identifiers
  • Personal identifiable information (PII)
  • Any data that could facilitate unauthorized access

Use Operating System Credential Managers

Modern operating systems provide secure credential storage mechanisms:

  • Windows: Credential Manager (DPAPI)
  • macOS: Keychain Services
  • Linux: Secret Service API (libsecret) or GNOME Keyring
// Example using electron-store with encryption
import Store from 'electron-store';

const store = new Store({
  encryptionKey: 'your-encryption-key',
  name: 'secure-credentials'
});

// Credentials are automatically encrypted
store.set('oauth_token', token);

Implement Defense in Depth

Don't rely on a single security measure:

  1. Encrypt credentials using strong algorithms (AES-256)
  2. Restrict file permissions to prevent unauthorized access
  3. Use secure key derivation (PBKDF2, Argon2, scrypt)
  4. Implement token rotation to limit exposure windows
  5. Add tamper detection using HMACs or authenticated encryption
  6. Audit access to credential storage locations

Security Testing and Detection

Use these tools and techniques to identify similar vulnerabilities:

Static Analysis Tools:
- Semgrep: Detect plaintext credential storage patterns
- SonarQube: Identify security hotspots in credential handling
- GitGuardian: Scan for exposed secrets in repositories

Example Semgrep Rule:

rules:
  - id: plaintext-credential-storage
    pattern: fs.writeFileSync($FILE, $CREDS)
    message: "Potential plaintext credential storage detected"
    severity: WARNING
    languages: [typescript, javascript]

Manual Code Review Checklist:
- [ ] Are credentials encrypted before writing to disk?
- [ ] Is a strong encryption algorithm used (AES-256)?
- [ ] Are encryption keys properly derived (PBKDF2/Argon2)?
- [ ] Are salts and IVs randomly generated per operation?
- [ ] Are file permissions restricted appropriately?
- [ ] Is there tamper detection (HMAC/authenticated encryption)?

Relevant Security Standards

This vulnerability relates to several security standards and guidelines:

  • OWASP Top 10 2021: A02:2021 – Cryptographic Failures
  • CWE-312: Cleartext Storage of Sensitive Information
  • CWE-522: Insufficiently Protected Credentials
  • NIST SP 800-63B: Digital Identity Guidelines (Authentication and Lifecycle Management)
  • PCI DSS Requirement 3.4: Render PAN unreadable anywhere it is stored

Additional Resources

Conclusion

The discovery and remediation of this plaintext credential storage vulnerability serves as an important reminder: security is not just about what you build, but how you build it. Having cryptographic capabilities available in your dependencies means nothing if they're not properly utilized.

Key takeaways for developers:

  1. Always encrypt sensitive data at rest, not just in transit
  2. Leverage existing security libraries rather than rolling your own crypto
  3. Use operating system credential managers when available
  4. Implement defense in depth with multiple security layers
  5. Regular security audits can catch these issues before they're exploited

Remember, every plaintext credential is a potential breach waiting to happen. The few extra lines of code to implement proper encryption could save your users—and your organization—from a devastating security incident.

Stay secure, encrypt your secrets, and always assume that local storage is accessible to attackers. Your users' security depends on it.


Have you found similar vulnerabilities in your codebase? Share your experiences and lessons learned in the comments below. And remember: when in doubt, encrypt!

Frequently Asked Questions

What is plaintext credential storage?

Plaintext credential storage (CWE-312) occurs when sensitive values like OAuth tokens or API keys are saved to disk, a database, or memory without any encryption, making them immediately readable by anyone who gains access to the storage location.

How do you prevent plaintext credential storage in filesystem caching?

Always encrypt credentials before writing them to disk using a strong key derivation function like PBKDF2, bcrypt, or Argon2. Use OS-level secure storage APIs (e.g., macOS Keychain, Windows DPAPI, Linux Secret Service) where available, and restrict file permissions to the owning user.

What CWE is plaintext credential storage?

Plaintext credential storage maps to CWE-312: Cleartext Storage of Sensitive Information, which covers any scenario where sensitive data is persisted without appropriate encryption or protection.

Is restricting file permissions enough to prevent plaintext credential storage?

No. File permissions reduce the attack surface but do not eliminate the risk. Malware running as the same user, backup systems, log aggregators, crash dump collectors, and forensic tools can all read permission-restricted files. Encryption ensures credentials are unreadable even if the file is accessed.

Can static analysis detect plaintext credential storage?

Yes. Static analysis tools like Semgrep, CodeQL, and Orbis AppSec can trace data flow from credential sources (OAuth token responses, API key configuration) to file-write sinks and flag cases where no encryption transformation occurs between the two.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #809

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.