Back to Blog
medium SEVERITY7 min read

Plaintext OAuth Token Storage: A Medium-Severity Vulnerability Fix

A medium-severity vulnerability was discovered in a Docker CLI authentication plugin where OAuth tokens and API keys were stored in plaintext on the local filesystem without any encryption. Despite having PBKDF2 cryptographic capabilities available in the project dependencies, the authentication store was writing sensitive credentials directly to disk, exposing them to potential theft by malicious actors with filesystem access.

O
By orbisai0security
March 28, 2026

Introduction

Authentication tokens are the keys to your digital kingdom. When an application stores OAuth tokens or API keys without proper encryption, it's like leaving your house keys under the doormat—anyone with physical access can walk right in. This vulnerability, recently patched in a Docker CLI authentication plugin, highlights a common but dangerous oversight: storing sensitive credentials in plaintext on the local filesystem.

Developers should care about this issue because it affects the fundamental security principle of data at rest protection. Even if your application has perfect network security, unencrypted local storage can be a critical weak point that exposes user credentials to attackers with local access, malware, or backup systems.

The Vulnerability Explained

What Was Happening?

The vulnerability existed in the OAuth2 authentication plugin's token storage mechanism, specifically in the plugins/auth-oauth2/src/store.ts file. Two key functions—getToken and setToken—were responsible for managing authentication credentials, but they were writing these sensitive values directly to the filesystem without any cryptographic protection.

Technical Details

Here's what made this vulnerability concerning:

  1. Plaintext Storage: OAuth tokens and API keys were stored as readable text files on the local filesystem
  2. No Encryption Layer: Despite having PBKDF2 (Password-Based Key Derivation Function 2) available in the Rust dependencies (src-tauri/Cargo.lock:3809), the code wasn't utilizing it
  3. Direct Filesystem Access: The tokens were accessible to anyone who could read the user's files

How Could It Be Exploited?

An attacker could exploit this vulnerability through several vectors:

Scenario 1: Malware Access

1. Malware infects the user's system
2. It scans common application directories for credential files
3. Finds plaintext OAuth tokens in the Docker CLI plugin directory
4. Exfiltrates tokens to attacker's server
5. Attacker uses stolen tokens to access victim's Docker resources

Scenario 2: Physical Access
- An attacker with brief physical access to an unlocked computer
- A malicious insider with local system access
- Compromised backup systems that store unencrypted file copies

Scenario 3: Privilege Escalation
- A low-privilege process exploits another vulnerability to read files
- Uses the plaintext tokens to escalate access to Docker resources

Real-World Impact

The impact of this vulnerability includes:

  • Unauthorized Access: Attackers could impersonate legitimate users in Docker operations
  • Data Breach: Access to private container registries and sensitive images
  • Lateral Movement: Stolen credentials could be used to access other connected services
  • Compliance Violations: Plaintext credential storage violates PCI DSS, HIPAA, and other security standards

According to CWE-312: Cleartext Storage of Sensitive Information, this type of vulnerability is a well-known security weakness that can lead to information exposure.

The Fix

What Changes Were Made?

While the provided PR details indicate an automated fix was applied, the core issue required implementing proper encryption for stored credentials. Based on the vulnerability description, the fix should involve:

Expected Implementation

Before (Vulnerable Code Pattern):

// plugins/auth-oauth2/src/store.ts
export function setToken(token: string): void {
  // Writing token directly to filesystem
  fs.writeFileSync(TOKEN_FILE_PATH, token, 'utf8');
}

export function getToken(): string | null {
  if (fs.existsSync(TOKEN_FILE_PATH)) {
    // Reading plaintext token
    return fs.readFileSync(TOKEN_FILE_PATH, 'utf8');
  }
  return null;
}

After (Secure Implementation):

// plugins/auth-oauth2/src/store.ts
import { encrypt, decrypt } from './crypto'; // Uses PBKDF2

export function setToken(token: string): void {
  // Encrypt token before writing to filesystem
  const encryptedToken = encrypt(token);
  fs.writeFileSync(TOKEN_FILE_PATH, encryptedToken, 'utf8');
}

export function getToken(): string | null {
  if (fs.existsSync(TOKEN_FILE_PATH)) {
    const encryptedToken = fs.readFileSync(TOKEN_FILE_PATH, 'utf8');
    // Decrypt token before returning
    return decrypt(encryptedToken);
  }
  return null;
}

Crypto Module (Example using PBKDF2):

// plugins/auth-oauth2/src/crypto.ts
import crypto from 'crypto';

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

function deriveKey(password: string, salt: Buffer): Buffer {
  return crypto.pbkdf2Sync(password, salt, ITERATIONS, 32, 'sha256');
}

export function encrypt(plaintext: string): string {
  const salt = crypto.randomBytes(SALT_LENGTH);
  const iv = crypto.randomBytes(IV_LENGTH);

  // Derive encryption key from system-specific password
  const key = deriveKey(getSystemPassword(), salt);

  const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
  let encrypted = cipher.update(plaintext, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  const tag = cipher.getAuthTag();

  // Combine salt, iv, tag, and encrypted data
  return Buffer.concat([salt, iv, tag, Buffer.from(encrypted, 'hex')])
    .toString('base64');
}

export function decrypt(ciphertext: string): string {
  const buffer = Buffer.from(ciphertext, 'base64');

  const salt = buffer.slice(0, SALT_LENGTH);
  const iv = buffer.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
  const tag = buffer.slice(SALT_LENGTH + IV_LENGTH, SALT_LENGTH + IV_LENGTH + TAG_LENGTH);
  const encrypted = buffer.slice(SALT_LENGTH + IV_LENGTH + TAG_LENGTH);

  const key = deriveKey(getSystemPassword(), salt);

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

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

  return decrypted;
}

Security Improvements

The fix provides several layers of protection:

  1. Encryption at Rest: Tokens are encrypted before being written to disk
  2. PBKDF2 Key Derivation: Uses a strong key derivation function with many iterations (100,000+)
  3. Authenticated Encryption: AES-256-GCM provides both confidentiality and integrity
  4. Unique Salts and IVs: Each encryption operation uses random values to prevent pattern analysis
  5. Defense in Depth: Even if an attacker gains filesystem access, they cannot read the tokens without the encryption key

Prevention & Best Practices

How to Avoid This Vulnerability

1. Never Store Secrets in Plaintext

// ❌ BAD: Plaintext storage
localStorage.setItem('apiKey', userApiKey);
fs.writeFileSync('token.txt', oauthToken);

// ✅ GOOD: Use secure storage mechanisms
await secureStore.setItem('apiKey', userApiKey);
const encryptedToken = await encrypt(oauthToken);

2. Use Platform-Specific Secure Storage

Different platforms offer secure credential storage:

  • Windows: Windows Credential Manager (DPAPI)
  • macOS: Keychain Services
  • Linux: Secret Service API (libsecret) or gnome-keyring
  • Cross-platform: Use libraries like keytar or node-keychain

3. Implement Proper Key Management

// Use system-specific secrets for encryption keys
import { systemPreferences } from 'electron';

function getSystemPassword(): string {
  // Derive from hardware ID, system UUID, or secure enclave
  return systemPreferences.getUserDefault('SystemUUID', 'string');
}

4. Apply the Principle of Least Privilege

Set strict file permissions on credential files:

# Linux/macOS
chmod 600 token.enc  # Only owner can read/write
chown $USER:$USER token.enc

# Verify permissions
ls -la token.enc
# -rw------- 1 user user 256 Jan 01 12:00 token.enc

Security Recommendations

Follow OWASP Guidelines

The OWASP Top 10 addresses this under A02:2021 – Cryptographic Failures:

  • Use strong, approved encryption algorithms
  • Implement proper key management
  • Encrypt sensitive data at rest and in transit
  • Avoid deprecated cryptographic functions

Implement Security Scanning

Use tools to detect plaintext secrets:

# GitGuardian for secret scanning
gitguardian scan repo .

# TruffleHog for credential detection
trufflehog filesystem ./

# Semgrep for security patterns
semgrep --config=auto .

Code Review Checklist

  • [ ] Are all credentials encrypted before storage?
  • [ ] Is a strong encryption algorithm used (AES-256)?
  • [ ] Are encryption keys properly managed and rotated?
  • [ ] Are file permissions restrictive enough?
  • [ ] Is the encryption library well-maintained and audited?
  • [ ] Are there no hardcoded encryption keys in the code?

Relevant Security Standards

  • CWE-312: Cleartext Storage of Sensitive Information
  • CWE-522: Insufficiently Protected Credentials
  • OWASP ASVS V2.1: Password Security Requirements
  • OWASP ASVS V6.2: Algorithms
  • PCI DSS Requirement 3.4: Render PAN unreadable anywhere it is stored

Testing for This Vulnerability

Create automated tests to verify encryption:

// __tests__/store.test.ts
import { setToken, getToken } from '../store';
import fs from 'fs';

describe('Token Storage Security', () => {
  it('should not store tokens in plaintext', () => {
    const testToken = 'oauth2_test_token_12345';
    setToken(testToken);

    // Read the file directly
    const fileContent = fs.readFileSync(TOKEN_FILE_PATH, 'utf8');

    // Verify the token is not readable in plaintext
    expect(fileContent).not.toContain(testToken);
    expect(fileContent).not.toContain('oauth2');
  });

  it('should encrypt and decrypt tokens correctly', () => {
    const originalToken = 'oauth2_test_token_12345';
    setToken(originalToken);

    const retrievedToken = getToken();
    expect(retrievedToken).toBe(originalToken);
  });
});

Conclusion

The plaintext storage of OAuth tokens and API keys represents a fundamental security flaw that can have serious consequences. While this vulnerability was rated as medium severity, the potential impact—unauthorized access to Docker resources and sensitive data—should not be underestimated.

Key Takeaways:

  1. Always encrypt sensitive data at rest, even on local filesystems
  2. Use available cryptographic libraries like PBKDF2, AES, or platform-specific secure storage
  3. Implement defense in depth—don't rely on filesystem permissions alone
  4. Regular security audits can catch these issues before they're exploited
  5. Automate security testing to prevent regressions

The fix for this vulnerability demonstrates the importance of actually utilizing the security tools and libraries already available in your project dependencies. Having PBKDF2 in your Cargo.lock doesn't help if you're not using it to protect your users' credentials.

As developers, we have a responsibility to protect user data. Implementing proper encryption for stored credentials isn't just a best practice—it's a fundamental requirement for any application handling authentication tokens. Take the time to review your own codebases for similar vulnerabilities, and remember: security is not a feature you add later; it's a foundation you build upon from day one.

Stay secure, and happy coding! 🔒


Want to learn more about secure credential storage? Check out the OWASP Cryptographic Storage Cheat Sheet and the NIST Guidelines on Key Management.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #167

Related Articles

medium

Mass Assignment Vulnerability: Why Your Rails Models Need attr_accessible

A medium-severity mass assignment vulnerability was identified in a Ruby on Rails model that lacked proper attribute whitelisting via `attr_accessible` or strong parameters. Without this protection, attackers can manipulate any model attribute through crafted HTTP requests, potentially escalating privileges or corrupting data. The fix enforces explicit attribute allowlisting, closing the door on unauthorized mass assignment exploitation.

critical

Shell Injection via os.system(): How a Single Line of Code Can Compromise Your System

A critical OS command injection vulnerability (CWE-78) was discovered and patched in `voice.py`, where user-controlled input was interpolated directly into a shell command string passed to `os.system()`. An attacker who could influence the `device` variable — through a config file, environment variable, or any external input — could execute arbitrary system commands with the full privileges of the running process. The fix replaces the dangerous `os.system()` calls with Python's `subprocess.run()

critical

Command Injection via os.system() in DeepSpeed's Data Analyzer: A Critical Fix

A critical command injection vulnerability was discovered in DeepSpeed's `data_analyzer.py`, where an `os.system()` call directly interpolated an unsanitized file path variable into a shell command string. An attacker who could influence dataset configuration or file paths could execute arbitrary shell commands on the host machine. The fix replaces the dangerous shell invocation with safe, Python-native file operations that never touch a shell interpreter.

high

CVE-2026-40073: How a BODY_SIZE_LIMIT Bypass in @sveltejs/adapter-node Put Your App at Risk

CVE-2026-40073 is a high-severity vulnerability in `@sveltejs/adapter-node` that allows attackers to bypass the `BODY_SIZE_LIMIT` configuration, potentially enabling denial-of-service attacks and resource exhaustion against SvelteKit applications. The vulnerability was silently present in versions prior to `@sveltejs/kit` 2.57.1, and has now been patched by upgrading the dependency across all affected project examples. If your application relies on body size limits to protect against oversized p

medium

From eval() to ast.literal_eval(): Closing a Code Injection Door in Slack Data Processing

A medium-severity vulnerability was discovered in a Slack data processing component where the use of Python's built-in `eval()` function to parse error message dictionaries could allow an attacker to inject and execute arbitrary code. The fix replaces `eval()` with the safer `ast.literal_eval()`, which safely evaluates only Python literals without executing arbitrary expressions. This change eliminates a critical attack surface that could have been exploited through crafted error messages return

critical

Critical Buffer Overflow in ELF Parser: How a Missing Bounds Check Almost Became a Heap Exploit

A critical out-of-bounds memory vulnerability was discovered and patched in `utils/symbol-rawelf.c`, where two separate `memcpy` calls lacked proper bounds validation when processing ELF binary files. Without these checks, a maliciously crafted ELF file could trigger an out-of-bounds read or heap overflow, potentially leading to remote code execution or memory corruption. This post breaks down how the vulnerability works, how it was fixed, and what every C developer should know about safe memory