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
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed 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

high

file_md5() MD5 Digest Lets Agent Docs Collide (CWE-328)

A first-party maintenance script used `hashlib.md5()` inside a `file_md5(path)` helper to decide whether the per-vendor agent instruction documents in a repository were byte-equivalent. Because MD5 is collision-broken, two meaningfully different documents could be crafted to produce the same digest and pass the "these files are in sync" gate. The fix replaces the MD5 call with `hashlib.sha256()` over the same `path.read_bytes()` input.

high

SQLite Auth Database Plaintext Storage in InitAuthDB()

The `InitAuthDB()` function opened SQLite databases without restricting filesystem permissions, leaving bcrypt password hashes, session tokens, and user settings exposed to any local account with read access. The fix explicitly sets `os.Chmod(filepath, 0600)` immediately after database creation, ensuring only the owner can access authentication secrets.

critical

Hybridauth Telegram OAuth Timing Attack in `authenticateCheckError()`

The Telegram OAuth provider in Hybridauth relied on `strcmp()` to validate HMAC-SHA256 signatures, exposing a timing side-channel that could allow attackers to forge authentication tokens character by character. The fix replaces this with PHP's timing-safe `hash_equals()` function, eliminating the information leak.

high

undici 7.29.0 TLS Bypass: BalancedPool Drops Connect Options

A critical flaw in undici's BalancedPool implementation silently discards TLS certificate validation settings when routing requests through certain connection paths. Attackers on the network could intercept HTTPS traffic without triggering validation errors. The fix preserves connect options throughout the connection lifecycle.

critical

Model Fetching Without Integrity Verification in ONNX Loading

A machine learning application was fetching ONNX model files and chunks over the network without verifying their integrity, creating an opening for model poisoning attacks. The fix adds cryptographic integrity verification at the point where downloaded chunks are reassembled and cached, ensuring models have not been modified in transit or at rest.

high

nanoid 3.3.11 Integer Overflow: Predictable ID Generation

An integer overflow in nanoid 3.3.11's internal randomness generation causes the library to fall back to predictable ID sequences, undermining the cryptographic guarantees of its supposedly unguessable identifiers. The fix upgrades the dependency tree to patched versions 3.3.12 or 5.1.11.