Back to Blog
medium SEVERITY6 min read

Plaintext OAuth Tokens: A Critical Security Flaw in Credential Storage

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 application's dependencies, credentials were written directly to disk, exposing users to potential token theft and unauthorized account access.

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

Answer Summary

Plaintext credential storage (CWE-256) occurs when OAuth tokens or API keys are written directly to the filesystem without encryption. In this case, the application had PBKDF2 cryptographic capabilities available but failed to use them, exposing credentials to local attackers. The fix encrypts all sensitive tokens using PBKDF2-derived keys before writing them to disk, ensuring that even if an attacker gains filesystem access, the credentials remain protected.

Vulnerability at a Glance

cweCWE-256
fixEncrypt credentials using PBKDF2 before filesystem storage
riskToken theft leading to unauthorized account access
languageApplication with filesystem credential storage
root causeOAuth tokens and API keys written to disk without encryption
vulnerabilityPlaintext Storage of Credentials

Introduction

Authentication credentials are the keys to your kingdom. When OAuth tokens and API keys are stored without proper protection, you're essentially leaving those keys under the doormat. This vulnerability, recently patched in the docreader application, demonstrates a common but dangerous security oversight: storing sensitive authentication data in plaintext on the local filesystem.

For developers, this serves as a critical reminder that having security tools available isn't enough—you must actually use them. This case is particularly interesting because the application had PBKDF2 (Password-Based Key Derivation Function 2) available in its Rust dependencies but wasn't leveraging it to protect stored credentials.

The Vulnerability Explained

What Happened?

The vulnerability existed in the OAuth2 authentication plugin, specifically in the plugins/auth-oauth2/src/store.ts file. The getToken() and setToken() functions were responsible for managing OAuth tokens and API keys, but they performed these operations without any cryptographic protection:

// Vulnerable pattern (conceptual example)
function setToken(token: string) {
    fs.writeFileSync('./credentials.json', JSON.stringify({ token }));
}

function getToken(): string {
    const data = fs.readFileSync('./credentials.json', 'utf8');
    return JSON.parse(data).token;
}

How Could It Be Exploited?

An attacker with access to the local filesystem could exploit this vulnerability in several ways:

  1. Direct File Access: Malware, physical access, or compromised backup systems could read the plaintext credential files
  2. Privilege Escalation: A low-privileged user on a shared system could access another user's tokens
  3. Forensic Recovery: Even deleted files could be recovered from disk, exposing historical credentials
  4. Cloud Sync Exposure: If the application directory syncs to cloud storage (Dropbox, OneDrive, etc.), tokens could leak to cloud servers

Real-World Impact

The consequences of this vulnerability include:

  • Account Takeover: Stolen OAuth tokens allow attackers to impersonate users
  • Data Breach: API keys could grant access to sensitive resources and data
  • Lateral Movement: Compromised credentials could be used to access other connected services
  • Compliance Violations: Plaintext credential storage violates PCI DSS, GDPR, and other regulatory requirements

Attack Scenario

Consider this realistic attack chain:

  1. A user installs the docreader application and authenticates with their Google account
  2. The OAuth token is stored in plaintext in ~/.docreader/auth/credentials.json
  3. The user's system is infected with info-stealing malware (a common threat)
  4. The malware scans common application directories and finds the plaintext token
  5. The attacker uses the stolen token to access the victim's Google account, email, and connected services
  6. The victim doesn't notice because the token is still valid—no re-authentication is required

The Fix

What Should Have Been Done?

The proper solution involves encrypting credentials before storing them on disk. Since PBKDF2 was already available in the Rust dependencies (src-tauri/Cargo.lock:3809), the fix should implement encryption using this proven cryptographic function.

Recommended Implementation

Here's how the fix should look conceptually:

Before (Vulnerable):

// Plaintext storage - INSECURE
function setToken(token: string) {
    const data = { token, timestamp: Date.now() };
    fs.writeFileSync(TOKEN_PATH, JSON.stringify(data));
}

function getToken(): string | null {
    if (!fs.existsSync(TOKEN_PATH)) return null;
    const data = JSON.parse(fs.readFileSync(TOKEN_PATH, 'utf8'));
    return data.token;
}

After (Secure):

import { invoke } from '@tauri-apps/api/tauri';

// Encrypted storage using Rust backend with PBKDF2
async function setToken(token: string) {
    // Derive encryption key from user-specific data
    const encryptedToken = await invoke('encrypt_token', { 
        plaintext: token 
    });

    fs.writeFileSync(TOKEN_PATH, encryptedToken);
}

async function getToken(): Promise<string | null> {
    if (!fs.existsSync(TOKEN_PATH)) return null;

    const encryptedToken = fs.readFileSync(TOKEN_PATH, 'utf8');

    // Decrypt using Rust backend
    const token = await invoke('decrypt_token', { 
        ciphertext: encryptedToken 
    });

    return token;
}

Rust Backend (src-tauri/src/crypto.rs):

use pbkdf2::{pbkdf2_hmac};
use aes_gcm::{Aes256Gcm, Key, Nonce};
use aes_gcm::aead::{Aead, NewAead};
use sha2::Sha256;

#[tauri::command]
fn encrypt_token(plaintext: String) -> Result<String, String> {
    // Derive key using PBKDF2
    let salt = get_device_specific_salt(); // Hardware-based or OS keychain
    let mut key = [0u8; 32];
    pbkdf2_hmac::<Sha256>(
        get_master_password().as_bytes(),
        &salt,
        100_000, // iterations
        &mut key
    );

    // Encrypt using AES-256-GCM
    let cipher = Aes256Gcm::new(Key::from_slice(&key));
    let nonce = Nonce::from_slice(b"unique nonce"); // Generate proper nonce

    let ciphertext = cipher.encrypt(nonce, plaintext.as_bytes())
        .map_err(|e| e.to_string())?;

    Ok(base64::encode(ciphertext))
}

#[tauri::command]
fn decrypt_token(ciphertext: String) -> Result<String, String> {
    // Similar decryption logic
    // ...
}

Security Improvements

This fix provides multiple layers of protection:

  1. Encryption at Rest: Credentials are encrypted before touching the filesystem
  2. Key Derivation: PBKDF2 with 100,000+ iterations makes brute-force attacks computationally expensive
  3. Device-Specific Protection: Using hardware-based or OS keychain data ties encryption to the specific device
  4. Authenticated Encryption: AES-GCM provides both confidentiality and integrity

Prevention & Best Practices

How to Avoid This Vulnerability

  1. Never Store Secrets in Plaintext: This is security 101, yet it remains a common mistake

  2. Use Platform Keychains: Leverage OS-provided secure storage:
    - macOS: Keychain Services
    - Windows: Credential Manager (DPAPI)
    - Linux: Secret Service API (libsecret)

  3. Implement Defense in Depth:
    typescript // Use multiple layers of protection - Encryption at rest (PBKDF2 + AES) - Restrictive file permissions (chmod 600) - OS keychain integration - Token rotation policies - Short-lived tokens when possible

  4. Apply the Principle of Least Privilege: Store only what you absolutely need

Security Recommendations

For OAuth Token Management:

  • Use refresh tokens instead of long-lived access tokens
  • Implement token rotation on every use
  • Set appropriate token expiration times
  • Clear tokens on logout
  • Never log tokens (even in debug mode)

For API Key Storage:

  • Use environment variables for server-side applications
  • Use secure vaults (HashiCorp Vault, AWS Secrets Manager) in production
  • Implement key rotation policies
  • Monitor for unauthorized key usage

Detection Tools

Prevent this vulnerability using:

  1. Static Analysis:
    - GitLeaks: Detect hardcoded secrets in code
    - TruffleHog: Scan git repositories for credentials
    - Semgrep: Custom rules for insecure storage patterns

  2. Code Review Checklist:
    ☐ Are credentials encrypted before storage? ☐ Is a strong encryption algorithm used (AES-256)? ☐ Is proper key derivation implemented (PBKDF2, Argon2)? ☐ Are file permissions restrictive? ☐ Is the OS keychain used when available?

  3. Runtime Monitoring:
    - File integrity monitoring (FIM)
    - Audit logs for credential access
    - Anomaly detection for unusual file access patterns

Security Standards & References

  • CWE-312: Cleartext Storage of Sensitive Information
  • CWE-522: Insufficiently Protected Credentials
  • OWASP Top 10 2021: A07:2021 – Identification and Authentication Failures
  • OWASP MASVS: MSTG-STORAGE-1 (Secure Storage)
  • PCI DSS: Requirement 3.4 (Render PAN unreadable)
  • NIST SP 800-63B: Digital Identity Guidelines (Authentication)

Conclusion

The plaintext credential storage vulnerability in docreader serves as an important lesson: security dependencies are useless if you don't use them. Having PBKDF2 available in the project's dependencies didn't protect users—only proper implementation could do that.

Key takeaways for developers:

  1. Encrypt sensitive data at rest—always, no exceptions
  2. Leverage existing security libraries rather than rolling your own crypto
  3. Use platform-specific secure storage when available
  4. Implement defense in depth with multiple security layers
  5. Regular security audits can catch these issues before they reach production

Remember: in security, good intentions aren't enough. The best encryption algorithm in the world provides zero protection if it's never invoked. Review your own applications today—are your credentials truly protected, or are they just one filesystem access away from compromise?

Stay secure, and always encrypt your secrets! 🔐


Have questions about secure credential storage? Found a similar vulnerability in your codebase? Share your experiences 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 storage media without encryption, making them readable by anyone with access to the storage location.

How do you prevent plaintext credential storage?

Encrypt credentials before storage using strong cryptographic algorithms like AES with keys derived from PBKDF2, use secure credential stores provided by the operating system, or leverage dedicated secrets management solutions.

What CWE is plaintext credential storage?

CWE-256 (Plaintext Storage of a Password) and the related CWE-312 (Cleartext Storage of Sensitive Information) cover vulnerabilities where credentials are stored without adequate protection.

Is file permission restriction enough to prevent credential theft?

No, file permissions alone are insufficient. Malware, privilege escalation attacks, or backup exposure can bypass permissions. Encryption provides defense-in-depth by protecting credentials even when filesystem access is compromised.

Can static analysis detect plaintext credential storage?

Yes, static analysis tools can detect patterns where credential-related variables are written to files without passing through encryption functions, though some cases require data flow analysis to identify.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #529

Related Articles

high

How Authentication Bypass happens in PyJWT and how to fix it

A critical authentication bypass vulnerability in PyJWT 2.12.1 allowed attackers to forge valid JSON Web Tokens, potentially bypassing application authentication mechanisms entirely. The vulnerability was fixed in PyJWT 2.13.0 through security improvements to token validation logic. This fix is essential for any application relying on JWT-based authentication.

high

How unsafe pickle deserialization happens in Keras/TensorFlow notebooks and how to fix it

A high-severity untrusted deserialization vulnerability was discovered in `TransferLearningTF.ipynb`, a transfer learning tutorial notebook that loads VGG16 model weights from the internet without verifying their integrity. Because Keras relies on Python's pickle-based serialization format under the hood, a tampered or substituted weights file could execute arbitrary code with the full privileges of the notebook user. The fix adds a SHA-256 checksum verification step immediately after the weight

critical

How Chromium launch-argument injection happens in Python Crawl4AI and how to fix it

A critical unauthenticated remote code execution vulnerability in Crawl4AI 0.8.9 allowed attackers to inject arbitrary Chromium launch arguments through the `browser_config.extra_args` parameter, potentially taking full control of the host process. The fix upgrades to Crawl4AI 0.9.0 and refactors the crawler initialization in `agent/tools/crawler.py` to use the new `BrowserConfig` and `CrawlerRunConfig` APIs, which enforce proper argument validation. This change eliminates the injection surface

critical

How integer overflow in buffer size calculation happens in C++ and how to fix it

A critical integer overflow vulnerability was discovered in OpenCV's HAL filter implementation where multiplying image dimensions without overflow protection could allocate dangerously undersized buffers. An attacker supplying crafted image dimensions (e.g., 65536×65536) could trigger heap corruption through out-of-bounds writes. The fix promotes the calculation to 64-bit arithmetic with a single cast.

critical

How buffer overflow via strcpy() happens in C zlib and how to fix it

A critical buffer overflow vulnerability was discovered in `general/libzlib/gzlib.c` where multiple `strcpy()` and `strcat()` calls operated without bounds checking. An attacker controlling file paths or error messages could overflow destination buffers, potentially achieving arbitrary code execution. The fix replaces these unsafe string operations with bounded `memcpy()` calls that respect pre-calculated buffer lengths.

critical

How hardcoded API key placeholders in documentation happen in Python and how to fix it

A high-severity security issue was discovered in the Context7 API documentation where a hardcoded API key placeholder (`CONTEXT7_API_KEY`) could be copied directly into production code. The fix replaced the static string with a proper environment variable reference using `os.environ`, preventing developers from accidentally deploying exposed credentials.