T09 · Insecure Skill Coding Practices
- Location
src/storage/tokens.ts:29- Finding
OAuth Tokens Written Through a Predictable and Initially Unprotected Temporary File
- Content
View full analysis
Vulnerability Details
File Location:
src/storage/tokens.ts, lines 29-34
Vulnerability Type: Insecure temporary file handling and sensitive token exposure
Risk Level: MediumVulnerable Code
ts export async function writeTokens(tokens: TokenData): Promise<void> { await ensureTokensDir(); const tempPath = `${TOKENS_PATH}.tmp`; await fs.writeFile(tempPath, JSON.stringify(tokens, null, 2), "utf8"); await fs.rename(tempPath, TOKENS_PATH); await fs.chmod(TOKENS_PATH, 0o600); }Technical Analysis
OAuth access and refresh tokens are first written to the predictable path
tokens.json.tmp. The temporary file is created without an explicit restrictive mode and without exclusive creation. Its initial permissions consequently depend on the process umask and may permit other local users to read it.The final file is changed to mode
0600only after the temporary file has been renamed. This creates a time-of-check/time-of-use exposure window. If the process terminates after the write or rename but beforechmod, sensitive tokens can remain stored with permissive permissions.Because the temporary pathname is fixed and
writeFiledoes not use thewxflag, a pre-existing file is overwritten. If an attacker can write to the configuration directory, the attacker may create a symbolic link attokens.json.tmp, causing the CLI to write token data to another user-writable target. Concurrent CLI processes can also interfere with one another through the shared temporary pathname.The containing directory is created at
src/storage/tokens.ts:13-15without an explicit0700mode, further relying on environmental defaults:ts export async function ensureTokensDir(): Promise<void> { await fs.mkdir(CONFIG_DIR, { recursive: true }); }Attack Path
- A local attacker identifies the predictable
~/.config/fitbit-cli/tokens.json.tmppath. - The victim runs ` ...[truncated 1328 chars]
- A local attacker identifies the predictable
- Remediation
View remediation
Remediation Suggestions
- Create and enforce the configuration directory with mode
0700, and verify that it is a real directory rather than a symbolic link. - Generate a cryptographically random temporary filename in the same directory as the destination.
- Open the temporary file using exclusive creation (
flag: "wx") and mode0600. - Write and flush the token data before atomically renaming the temporary file to
tokens.json. - Ensure the final file has mode
0600before making it available at the final pathname. - Remove the temporary file on every failure path using a
try/finallyblock. - Reject unexpected symbolic links and non-regular files when reading or replacing token storage.
- Serialize token updates or use unique temporary files to prevent concurrent processes from racing.
Example hardened creation options:
ts await fs.mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 }); const tempPath = path.join( CONFIG_DIR, `.tokens-${process.pid}-${crypto.randomBytes(16).toString("hex")}.tmp`, ); try { await fs.writeFile( tempPath, JSON.stringify(tokens, null, 2), { encoding: "utf8", mode: 0o600, flag: "wx" }, ); await fs.rename(tempPath, TOKENS_PATH); await fs.chmod(TOKENS_PATH, 0o600); } finally { await fs.unlink(tempPath).catch(() => undefined); }- Create and enforce the configuration directory with mode
