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 Orbis AppSec
Published March 28, 2026Reviewed June 3, 2026

Answer Summary

This vulnerability is a case of plaintext credential storage (CWE-312) in a Docker CLI authentication plugin written to manage OAuth tokens and API keys. The authentication store wrote sensitive credentials directly to the local filesystem without encryption, despite PBKDF2 cryptographic functions being available in the project's dependencies. The fix encrypts credentials using PBKDF2 before writing them to disk and decrypts them on read, ensuring that filesystem access alone is not sufficient to steal valid tokens.

Vulnerability at a Glance

cweCWE-312
fixEncrypt credentials with PBKDF2 before writing to disk; decrypt on read
riskOAuth tokens and API keys exposed to any user or process with filesystem read access
languageGo (Docker CLI plugin)
root causeAuthentication store wrote credentials directly to disk without invoking available PBKDF2 encryption
vulnerabilityPlaintext OAuth Token Storage

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.

Frequently Asked Questions

What is plaintext credential storage?

Plaintext credential storage (CWE-312) occurs when an application writes sensitive data such as passwords, tokens, or API keys to persistent storage—files, databases, or logs—without encrypting them first, making them readable by anyone with access to that storage medium.

How do you prevent plaintext credential storage in Go?

Use a well-supported key derivation function such as PBKDF2 (golang.org/x/crypto/pbkdf2) or Argon2 to derive an encryption key from a secret, then encrypt credentials with AES-GCM before writing them to disk. Never store raw tokens or keys in configuration files or credential stores.

What CWE is plaintext credential storage?

Plaintext credential storage maps to CWE-312: Cleartext Storage of Sensitive Information. Related identifiers include CWE-256 (Plaintext Storage of a Password) and CWE-522 (Insufficiently Protected Credentials).

Is restricting file permissions enough to prevent credential theft?

No. File permissions reduce the attack surface but do not eliminate the risk. Privileged processes, container escapes, backup exfiltration, and misconfigured permissions can all expose the file. Encryption ensures that even if the file is read, the credentials are not immediately usable.

Can static analysis detect plaintext credential storage?

Yes. Static analysis tools such as Semgrep, CodeQL, and Orbis AppSec can trace credential values from their source (e.g., an OAuth token response) to a sink (e.g., a file write call) and flag cases where no encryption step is present in between.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #167

Related Articles

medium

How insecure update manifest parsing happens in C++ UpdateHelper.cpp and how to fix it

TrafficMonitor's software update mechanism in `UpdateHelper.cpp` fetched and parsed update manifests from remote servers without validating the version string or enforcing trusted download URLs, leaving users exposed to man-in-the-middle (MITM) attacks. An attacker on the same network could intercept the update channel and inject a malicious binary under a crafted version string or an HTTP download link pointing to attacker-controlled infrastructure. The fix adds strict version-string sanitizati

high

How integer overflow in malloc happens in C bipartite matching and how to fix it

A high-severity integer overflow vulnerability was discovered in the bipartite matching algorithm implementation where unchecked multiplication operations for memory allocation could wrap around, causing undersized buffer allocations and subsequent heap overflow. The fix replaces vulnerable `malloc(sizeof(int) * V)` patterns with safe `calloc(V, sizeof(int))` calls and adds proper bounds validation to prevent exploitation.

high

How integer truncation heap overflow happens in C++ UEFI ACPI parsing and how to fix it

A high-severity integer truncation vulnerability was discovered in `Mobility.Uefi.Acpi.cpp` where heap allocation sizes were stored in a 16-bit integer (`MO_UINT16`), causing silent truncation when the computed size exceeded 65535 bytes. This led to undersized heap allocations followed by out-of-bounds writes, exploitable by an attacker who can influence ACPI SRAT table contents in virtualized environments. The fix promotes the size variable to `MO_UINTN` (platform-native width) to prevent trunc

critical

How API key exposure in configuration files happens in TOML config and how to fix it

A critical security vulnerability in `commands/webperf.toml` allowed API keys to be hardcoded directly in configuration files, creating a credential exposure risk. The documentation on line 11 suggested developers could provide `CRUX_API_KEY` or `GOOGLE_API_KEY` directly in the config, which could lead to these sensitive credentials being committed to version control or exposed in logs. The fix updated the documentation to explicitly require environment variables and warn against hardcoding cred

high

How path traversal happens in Ruby YARD server and how to fix it

A high-severity path traversal vulnerability (CVE-2026-41493) in YARD versions prior to 0.9.42 allowed attackers to read arbitrary files from servers running `yard server`. This fix upgrades the yard gem from 0.9.26 to 0.9.42 in the Gemfile and Gemfile.lock, closing a dangerous information disclosure vector that could expose configuration files, credentials, and source code.

high

How buffer overflow via sprintf() happens in C networking code and how to fix it

A high-severity buffer overflow vulnerability was discovered in `profile.c` where `sprintf()` was used to format server addresses without any bounds checking. An attacker who could influence the `SERVER_BASE_PORT` value or trigger integer overflow in the port calculation could write beyond the `server_address` buffer. The fix replaces `sprintf()` with `snprintf()` using explicit buffer size limits at both call sites (lines 99 and 220).