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

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.