Skip to content

[Security] HIGH: OAuth Tokens Stored in Plaintext on Disk #31

@tembo

Description

@tembo

Summary

OAuth access tokens and refresh tokens are stored in plaintext JSON on disk, creating a significant security risk if the user's machine is compromised.

Severity

HIGH

Affected Files

  • src/services/x-auth.ts (Line 191)

Code Snippet

writeFileSync(this.tokensPath, JSON.stringify(store, null, 2), { mode: 0o600 });

Security Risk

While file permissions are correctly set to 0o600, tokens are stored unencrypted:

  • If the machine is compromised, tokens are immediately accessible
  • Tokens may be backed up or synced to cloud storage
  • No encryption-at-rest protection
  • Stored at .shippost-tokens.json in predictable location

Potential Impact

  • Complete Twitter/X account compromise if tokens are stolen
  • Ability to post, read, and modify content as the user
  • Long-lived access if refresh token is stolen

Recommended Fix

Implement encryption for tokens at rest:

import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';

class SecureTokenStorage {
  private getEncryptionKey(): Buffer {
    // Use machine-specific key derivation
    const machineId = getMachineId(); // Use a machine fingerprint
    return scryptSync(machineId, 'shippost-salt', 32);
  }

  encryptTokens(tokens: XTokens): string {
    const key = this.getEncryptionKey();
    const iv = randomBytes(16);
    const cipher = createCipheriv('aes-256-gcm', key, iv);
    const encrypted = Buffer.concat([
      cipher.update(JSON.stringify(tokens), 'utf8'),
      cipher.final()
    ]);
    const authTag = cipher.getAuthTag();
    return Buffer.concat([iv, authTag, encrypted]).toString('base64');
  }

  decryptTokens(encrypted: string): XTokens {
    const key = this.getEncryptionKey();
    const data = Buffer.from(encrypted, 'base64');
    const iv = data.subarray(0, 16);
    const authTag = data.subarray(16, 32);
    const ciphertext = data.subarray(32);
    const decipher = createDecipheriv('aes-256-gcm', key, iv);
    decipher.setAuthTag(authTag);
    const decrypted = Buffer.concat([
      decipher.update(ciphertext),
      decipher.final()
    ]);
    return JSON.parse(decrypted.toString('utf8'));
  }
}

Labels

security, vulnerability, authentication

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions