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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #11

Related Articles

critical

How insufficient PBKDF2 iterations happen in JavaScript and how to fix it

A critical vulnerability in `libs/wgs/pbkdf2.js` used only 1 iteration for PBKDF2 password hashing, making passwords trivially crackable. The fix increases iterations to 600,000, aligning with OWASP 2023 recommendations and preventing GPU-accelerated brute-force attacks.

critical

How Hardcoded Encryption Salts Compromise Credential Storage in Node.js and How to Fix It

A critical vulnerability in `scripts/bench-cpu.js` used a hardcoded static salt (`'byok-relay-salt'`) when deriving encryption keys with scrypt, allowing attackers to decrypt all encrypted credentials if the encryption secret was compromised. The fix replaces the hardcoded salt with cryptographically secure random bytes generated per operation, ensuring each user's encrypted credentials require a unique derived key.

high

How weak scrypt password hashing happens in Node.js and how to fix it

The `hashPass` function in `store-saas/server.mjs` used Node.js's `crypto.scryptSync` with default cost parameters (N=16384, r=8, p=1), making stored password hashes cheap to attack with modern GPUs. The fix increases the CPU/memory cost factor to N=131072 and parallelization to p=2, dramatically raising the computational effort required to brute-force stolen hashes.

high

How Dependency Version Pinning Prevents Supply Chain Attacks in Node.js and How to Fix It

A critical supply chain vulnerability in `package.json` allowed automatic updates to a cryptographic library with known weaknesses. By pinning `rijndael-js` to version `2.0.0` instead of allowing `^2.0.0` updates, the fix prevents silent installation of vulnerable versions that could expose downstream consumers to weak block cipher modes and authentication bypasses.

critical

How Insecure Randomness in form-data happens in Node.js and how to fix it

The `form-data` npm package, pinned at `^2.3.3` in `server/package-lock.json`, generated multipart form boundaries using the insecure `Math.random()` function instead of a cryptographically secure random source. This predictable boundary generation (CVE-2025-7783) could allow an attacker to guess or influence multipart boundaries, opening the door to request smuggling and payload injection in HTTP requests built by the server.

high

How Interpretation Conflict Vulnerability happens in Node.js and how to fix it

node-forge versions up to 1.3.1 shipped an ASN.1 parser vulnerable to an interpretation conflict that could let attackers bypass cryptographic signature verification, alongside a related unbounded recursion flaw (CVE-2025-66031) that enables denial-of-service. Upgrading the dependency to node-forge 1.4.0 patches both issues by hardening the ASN.1 decoder against malformed and adversarially crafted input.