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 Denial of Service via infinite loop happens in Node.js dependencies and how to fix it

A high-severity Denial of Service vulnerability in the nanoid package (CVE-2026-67213) was discovered in the project's dependency tree, where crafted input could trigger an infinite loop during random ID generation. The fix upgrades nanoid from 3.3.17 to 3.3.18 and adds an npm override to ensure all transitive dependencies use the patched version.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A Dependabot configuration in `.github/dependabot.yml` was missing cooldown periods for both its npm and GitHub Actions package ecosystems, meaning newly published — potentially malicious or unstable — package versions could be proposed for adoption immediately after release. Adding a `cooldown` block with `default-days: 7` to each ecosystem entry creates a 7-day buffer, allowing the security community time to identify and flag compromised packages before they reach your codebase.

high

How pnpm Missing Minimum Release Age happens in Node.js workspaces and how to fix it

A missing `minimumReleaseAge` setting in `pnpm-workspace.yaml` left this Node.js workspace vulnerable to immediately installing newly published — potentially malicious — package versions. The fix adds `minimumReleaseAge: 10080` (7 days in minutes) to enforce a quarantine window before any freshly published package can be installed. This single configuration change significantly reduces the risk of supply chain attacks targeting the package publishing pipeline.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A high-severity misconfiguration in `.github/dependabot.yml` left three `package-ecosystem` entries without a cooldown period, meaning Dependabot could immediately propose updates from newly published—potentially malicious—packages. The fix adds a `cooldown` block with `default-days: 7` to each entry, introducing a mandatory waiting period before any newly released package version is surfaced as an update candidate. For a Node.js library whose vulnerabilities ripple downstream to all consumers,

critical

How Unauthenticated Proxy Endpoints Enable DoS Amplification in FastAPI and how to fix it

Public proxy endpoints in `backend/api/proxy.py` had no rate limiting, allowing any attacker to flood the httpx connection pool with unauthenticated requests and amplify denial-of-service attacks against downstream tile and coordinate-conversion services. The fix introduces a per-IP sliding-window rate limiter using environment-configurable thresholds, closing the amplification vector without breaking legitimate usage.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A missing `cooldown` block in `.github/dependabot.yml` meant that Dependabot could immediately propose updates to newly published npm packages — including those that may be malicious, compromised, or unstable. By adding a `cooldown` with `default-days: 7`, the project now waits one week before surfacing new package versions, giving the security community time to detect and flag bad releases before they reach production.