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 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.