Back to Blog
critical SEVERITY9 min read

How Plaintext Token Storage happens in TypeScript/Tauri and how to fix it

A critical vulnerability in a Tauri desktop application allowed GitHub API tokens with full `repo` scope to be written to plaintext local storage files via the `getAllSettings()` function in `src/config/settings.ts`. Any process with filesystem access — including malware, other apps, or a logged-in attacker — could silently extract these tokens. The fix introduces a `SENSITIVE_KEYS` exclusion set that prevents credentials from being serialized to disk.

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

This is a plaintext credential storage vulnerability (CWE-312) in a TypeScript/Tauri desktop application, where GitHub API tokens stored under the key `cloudArticleToken` were written to disk in cleartext via the `getAllSettings()` function in `src/config/settings.ts`. The fix adds a `SENSITIVE_KEYS` set containing `'cloudArticleToken'` and filters those keys out of the disk-serialization path in `getAllSettings()`, ensuring sensitive credentials are never written to plaintext storage files.

Vulnerability at a Glance

cweCWE-312
fixIntroduced a `SENSITIVE_KEYS` exclusion set; filtered those keys from the disk-serialization path in `getAllSettings()`
riskGitHub tokens with 'repo' scope exposed to any process with filesystem read access
languageTypeScript
root cause`getAllSettings()` serialized all settings keys — including `cloudArticleToken` — to a plaintext JSON file on disk
vulnerabilityCleartext Storage of Sensitive Information

How Plaintext Token Storage Happens in TypeScript/Tauri and How to Fix It


The Vulnerability at a Glance

Field Detail
Vulnerability Cleartext Storage of Sensitive Information
CWE CWE-312
Severity Critical
File src/config/settings.ts
Affected Key cloudArticleToken (GitHub API token, repo scope)
Fix Filter sensitive keys from disk-serialization in getAllSettings()

Introduction

The src/config/settings.ts file is the central hub for persisting user preferences in this Tauri desktop application — it handles everything from UI preferences to API credentials. But a flaw in the getAllSettings() function created a serious security risk: every time settings were flushed to disk via syncToDisk(), the GitHub API token stored under the key cloudArticleToken was written out in plaintext JSON right alongside harmless preferences like font size or theme color.

GitHub tokens with repo scope are among the most powerful credentials a developer can hold. They grant read and write access to every private repository the user owns. Storing them in a cleartext settings file — indistinguishable from any other application data — means that any process, script, or piece of malware that can read the filesystem can silently harvest a fully-privileged GitHub token without the user ever knowing.


The Vulnerability Explained

What Was Happening

The getAllSettings() function in src/config/settings.ts iterated over every key in DEFAULT_SETTINGS and included its value in the object that gets written to disk:

// BEFORE — vulnerable code
export function getAllSettings(): Record<string, unknown> {
  const result: Record<string, unknown> = {}
  for (const key of Object.keys(DEFAULT_SETTINGS)) {
    result[key] = getSetting(key)   // ← no distinction between sensitive and non-sensitive keys
  }
  return result
}

This function is called by syncToDisk(), which serializes the result to a local JSON file. The cloudArticleToken key — which holds a GitHub OAuth token with repo scope — was included in that serialization without any filtering or encryption.

The resulting file on disk would look something like this:

{
  "theme": "dark",
  "language": "zh-CN",
  "cloudArticleToken": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "fontSize": 14
}

A GitHub Personal Access Token (PAT) sitting in a JSON file, right next to display preferences.

Why This Is Particularly Dangerous

Scope amplification: A repo-scoped GitHub token isn't just a read credential. It can clone private repositories, push malicious commits, create webhooks to exfiltrate future code, and access GitHub Actions secrets. Stealing one token can compromise an entire organization's codebase.

Passive exfiltration: Unlike an active network attack, reading a local file requires no special privileges beyond normal user-level filesystem access. Any other application running as the same OS user — including browser extensions, npm scripts in a postinstall hook, or malware — can read this file without triggering security alerts.

No expiry signal: The user has no indication the token was read. There's no authentication log entry, no GitHub audit event, nothing. The attacker can use the token indefinitely until it's manually revoked.

A Concrete Attack Scenario

Consider this attack chain:

  1. A developer installs a malicious npm package that includes a postinstall script.
  2. The script runs as the developer's OS user and enumerates common Tauri application data directories (e.g., ~/.local/share/<appname>/ on Linux, %APPDATA%\<appname>\ on Windows).
  3. It finds the settings JSON file, reads cloudArticleToken, and exfiltrates it to an attacker-controlled server over HTTPS.
  4. The attacker now has full repo access to all of the developer's private GitHub repositories.

The entire attack takes milliseconds and leaves no trace in application logs.


The Fix

What Changed

The fix is surgical and elegant. A single SENSITIVE_KEYS set is introduced, and getAllSettings() is updated to skip any key present in that set:

// AFTER — fixed code
const SENSITIVE_KEYS: Set<string> = new Set(['cloudArticleToken'])

/** 导出当前所有设置(用于写入磁盘 JSON) */
export function getAllSettings(): Record<string, unknown> {
  const result: Record<string, unknown> = {}
  for (const key of Object.keys(DEFAULT_SETTINGS)) {
    if (!SENSITIVE_KEYS.has(key)) result[key] = getSetting(key)  // ← sensitive keys excluded
  }
  return result
}

Before vs. After

Before:

for (const key of Object.keys(DEFAULT_SETTINGS)) {
  result[key] = getSetting(key)
}

After:

const SENSITIVE_KEYS: Set<string> = new Set(['cloudArticleToken'])

for (const key of Object.keys(DEFAULT_SETTINGS)) {
  if (!SENSITIVE_KEYS.has(key)) result[key] = getSetting(key)
}

Why This Fix Works

The getAllSettings() function is the only path through which in-memory settings reach syncToDisk(). By filtering cloudArticleToken out of the returned object before it ever reaches the serialization layer, the token is guaranteed never to appear in the on-disk JSON file — regardless of how syncToDisk() is called or how frequently settings are flushed.

The fix also establishes a clear, maintainable pattern: the SENSITIVE_KEYS set is a single place to register credentials. When a new sensitive setting is added in the future (an API key for a different service, for example), developers only need to add it to this set rather than hunting for every serialization call site.

What This Fix Does Not Change

The fix is deliberately scoped. It does not affect:
- How cloudArticleToken is read back into memory via getSetting()
- How setSetting() stores the token in the in-memory settings store
- Any non-sensitive settings, which continue to be persisted normally

Valid application behavior is fully preserved; only the credential leak path is closed.


Prevention & Best Practices

1. Use the OS Keychain for Long-Lived Secrets

The gold standard for storing credentials in a desktop application is the operating system's secure credential store:
- macOS: Keychain Services
- Windows: Windows Credential Manager (via DPAPI)
- Linux: libsecret / GNOME Keyring

Tauri exposes these through the tauri-plugin-stronghold and community crates. Secrets stored in the OS keychain are encrypted at rest and access-controlled by the OS, not just filesystem permissions.

2. Never Serialize Credentials Alongside Non-Sensitive Settings

Keep a strict architectural separation between "preferences" (safe to persist as JSON) and "credentials" (must go through a secure store). The SENSITIVE_KEYS pattern introduced in this fix is a good intermediate step, but the long-term goal should be to route credentials through a dedicated, encrypted channel entirely.

3. Audit All Disk-Write Paths

In any settings or configuration system, identify every function that writes data to disk and ensure each one either:
- Explicitly excludes sensitive keys (as this fix does), or
- Encrypts the entire file using a key derived from a hardware-bound or user-provided secret

4. Apply the Principle of Least Privilege to Token Scopes

If the application only needs to read public repository data, request a token with public_repo scope rather than the full repo scope. A stolen narrow-scope token causes significantly less damage.

5. Leverage PBKDF2 Already in Your Dependency Tree

The project's src-tauri/Cargo.lock already includes PBKDF2 (line 3809). This means the infrastructure for key derivation is already present. A future improvement would be to derive an encryption key from a user-provided passphrase or hardware identifier using PBKDF2, then encrypt stored credentials with that key before any disk write.

OWASP & CWE Alignment

  • CWE-312: Cleartext Storage of Sensitive Information
  • CWE-313: Cleartext Storage in a File or on Disk
  • CWE-522: Insufficiently Protected Credentials
  • OWASP A02:2021: Cryptographic Failures
  • OWASP ASVS v4.0, Section 6.1: Data Classification — credentials must be stored with appropriate cryptographic protection

Key Takeaways

  • getAllSettings() was the single dangerous serialization sink — all sensitive credential leakage flowed through this one function, making it the right and minimal place to apply the fix.
  • GitHub tokens with repo scope are organization-level blast radius — a single leaked token can expose every private repository a developer has access to, not just their own projects.
  • A SENSITIVE_KEYS exclusion set is a scalable pattern — it creates a single, auditable registry of credentials that must never reach disk, making future additions safe and reviewable.
  • Tauri's getSetting/setSetting API does not encrypt by default — developers building Tauri apps must explicitly architect credential storage to avoid this class of vulnerability; the framework does not protect you automatically.
  • PBKDF2 was already available in src-tauri/Cargo.lock — the cryptographic primitives needed for proper at-rest encryption were present but unused, illustrating that having a dependency is not the same as using it securely.

How Orbis AppSec Detected This

  • Source: The setSetting('cloudArticleToken', value) call, which places a GitHub API token into the in-memory settings store.
  • Sink: getAllSettings() in src/config/settings.ts, which unconditionally includes all settings keys in the object passed to syncToDisk(), writing them to a plaintext JSON file on disk.
  • Missing control: No exclusion, redaction, or encryption was applied to the cloudArticleToken key before the settings object reached the disk-write path.
  • CWE: CWE-312 — Cleartext Storage of Sensitive Information.
  • Fix: A SENSITIVE_KEYS: Set<string> containing 'cloudArticleToken' was introduced, and getAllSettings() was updated to skip any key present in that set before building the disk-serialization object.

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

The vulnerability fixed here is a textbook example of how a single missing guard in a serialization function can expose the most sensitive credentials in an application. The getAllSettings() function in src/config/settings.ts had no mechanism to distinguish between a harmless UI preference and a fully-privileged GitHub API token — so it wrote both to disk with equal indifference.

The fix is minimal, targeted, and immediately effective: a SENSITIVE_KEYS set and a one-line conditional in the loop body. But the deeper lesson is architectural. In any application that handles credentials, every path that leads to persistent storage must be treated as a potential leak point and audited explicitly. The presence of PBKDF2 in the project's Rust dependencies shows that the building blocks for proper encryption were always available — the gap was in applying them.

For developers building Tauri applications or any desktop app that handles OAuth tokens or API keys: treat your local settings file as a public document. If you wouldn't want to commit a value to a public GitHub repository, it should never appear in a plaintext settings file.


References

Frequently Asked Questions

What is cleartext storage of sensitive information?

Cleartext storage means credentials or secrets are written to disk, a database, or logs without any encryption or obfuscation, making them trivially readable by any process or user with filesystem access.

How do you prevent cleartext credential storage in TypeScript/Tauri?

Never include sensitive keys in disk-serialization paths. Use an exclusion list (like a `SENSITIVE_KEYS` set) to filter credentials before writing settings to disk, and prefer OS-level secure storage APIs (e.g., the system keychain) for long-lived secrets.

What CWE is cleartext credential storage?

CWE-312: Cleartext Storage of Sensitive Information. Related entries include CWE-313 (Cleartext Storage in a File or on Disk) and CWE-522 (Insufficiently Protected Credentials).

Is encrypting the settings file enough to prevent this vulnerability?

File encryption helps but is not sufficient on its own. If the decryption key is stored alongside the file or derived from a weak source, attackers can still recover the plaintext. The most robust approach is to store secrets only in the OS keychain and never write them to the settings file at all.

Can static analysis detect cleartext credential storage?

Yes. Tools like Semgrep, CodeQL, and Orbis AppSec can trace data flow from credential-assignment calls (e.g., `setSetting('cloudArticleToken', ...)`) to disk-write sinks (e.g., `syncToDisk()`) and flag cases where no sanitization or exclusion logic is present on the sensitive key.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #11

Related Articles

critical

How Unsafe Random Functions Happen in Node.js Form Data and How to Fix It

CVE-2025-7783 is a critical vulnerability in the `form-data` npm package caused by the use of an unsafe random number generator to produce multipart form boundaries, making those boundaries predictable by an attacker. The fix upgrades `form-data` to versions 2.5.4, 3.0.4, and 4.0.4, which replace the weak random function with a cryptographically secure alternative. This change was applied to the `example-apps/collector/package-lock.json` and `package.json` files in the Instana collector example

critical

How Weak Randomness Happens in Node.js WS-Security and How to Fix It

A critical vulnerability in `src/security/WSSecurity.ts` used `Math.random()` to generate nonces for WS-Security UsernameToken authentication, making nonces statistically predictable and defeating replay protection. By replacing the insecure SHA1-hashed random value with `crypto.randomBytes(16)`, the fix ensures nonces are cryptographically unpredictable. This change protects all downstream consumers of this Node.js SOAP library from nonce-prediction attacks on WS-Security authenticated endpoint

critical

How Implicit TLS Certificate Verification Happens in Python and How to Fix It

A critical security vulnerability was discovered in `plugins/python-build/scripts/add_cpython.py` where `requests.get()` calls to the GitHub API and OpenSSL release endpoints lacked explicit TLS certificate verification enforcement and consistent error handling. While Python's `requests` library defaults to `verify=True`, the absence of explicit enforcement and centralized error handling left the build tool exposed to man-in-the-middle attacks that could inject malicious package data. The fix in

critical

How Unauthenticated API Endpoints happen in Node.js Express and how to fix it

The `/token` endpoint in `plugin/multiplex/index.js` generated presentation control tokens without verifying the requester's identity, allowing any attacker with network access to seize control of a live reveal.js presentation. The fix restricts token generation to localhost-only requests and replaces a broken cryptographic primitive with a proper SHA-256 hash. Together, these changes eliminate both the access-control gap and a secondary cryptographic weakness in a single targeted patch.

critical

How Unsafe Random Functions Happen in Node.js form-data and How to Fix It

CVE-2025-7783 is a critical vulnerability in the `form-data` npm package caused by its use of an unsafe random function to generate multipart form boundaries. This flaw allows attackers to predict boundary values, potentially enabling them to manipulate or inject content into multipart requests. The fix upgrades `form-data` to version 4.0.6 and enforces this version across the entire dependency tree using a `package.json` `overrides` directive.

critical

How eval() Code Injection happens in JavaScript and how to fix it

A critical code injection vulnerability was discovered in `js/lib/jsencrypt.js` at line 195, where a direct `eval()` call executed a JavaScript string shim for the `process` object in browser environments. If an attacker could influence the string passed to `eval()`—through a compromised dependency, a man-in-the-middle attack, or supply chain tampering—they could achieve arbitrary JavaScript execution in any user's browser. The fix replaces the `eval()` call with the equivalent inline JavaScript