Back to Blog
high SEVERITY6 min read

How Insufficiently Protected Credentials happens in Node.js and how to fix it

A regex-based guard in `skills/xmemo/scripts/xmemo-skill.mjs` was supposed to block sensitive credentials from being passed as CLI flags, but it only matched a handful of keyword patterns—missing `password`, `client-secret`, `access-token`, `xmemo-key`, and more. The fix expands the blocklist regex to cover these additional credential patterns, closing a gap that could let secrets end up in shell history and process listings.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published September 7, 2026•Reviewed September 7, 2026

Answer Summary

This is an Insufficiently Protected Credentials vulnerability (CWE-522) in a Node.js CLI tool, where the sensitive-flag detection regex in `validateCommandInput()` was too narrow to catch common credential flag names like `--password` or `--access-token`. The fix broadens the regex pattern to match additional credential keywords, ensuring more secret-like CLI options are rejected before they can be captured in shell history or `ps aux` output.

Vulnerability at a Glance

cweCWE-522
fixExpanded the blocklist regex in `xmemo-skill.mjs` to include password, credential, client-secret, refresh-token, access-token, private-key, and xmemo-key patterns
riskCredentials passed via CLI flags or environment variables can be captured via shell history, process listings, or plaintext credential files
languageJavaScript (Node.js, ESM)
root causeOverly narrow regex in `validateCommandInput()` failed to block common credential-related flag names
vulnerabilityInsufficiently Protected Credentials

Introduction

The skills/xmemo/scripts/xmemo-skill.mjs file implements a command-line skill for the xmemo tool, and part of its job is guarding against secrets accidentally leaking through CLI arguments. Inside validateCommandInput(), a regex was used to detect and reject flag names that look like credentials — things like --token or --api-key. The intent was solid: stop users (or scripts) from typing secrets directly into a command line where they could be captured by shell history, ps aux, or terminal scrollback.

The problem was in the details. At line 386, the regex only matched a limited set of keyword patterns:

if (/^(token|api[-_]?key|bearer|authorization|cookie|secret)$/i.test(key) && key !== 'from-stdin') {
  throw new Error(`Refusing sensitive command-line option --${key}. Use XMEMO_KEY or --from-stdin where documented.`);
}

This blocked --token, --api-key, --bearer, --authorization, --cookie, and --secret — but nothing stopped a user from passing --password, --client-secret, --access-token, --refresh-token, --private-key, or even --xmemo-key directly on the command line. Any developer extending this tool, or any script automating xmemo calls, could unintentionally leak a credential through one of these unguarded flag names.

The Vulnerability Explained

The core issue is an incomplete allowlist/blocklist. Security-relevant regexes that try to enumerate "bad" input are only as strong as their coverage, and here the coverage had clear gaps.

Before the fix, this regex governed what counted as a sensitive flag:

/^(token|api[-_]?key|bearer|authorization|cookie|secret)$/i

Consider an automation script or a careless user running:

xmemo-skill sync --password=SuperSecretPass123

Because password wasn't in the pattern, this flag would sail past the check. The credential would then:

  1. Land in the shell's history file (.bash_history, .zsh_history), often stored in plaintext indefinitely.
  2. Be visible to any local user running ps aux or /proc/<pid>/cmdline while the process is executing.
  3. Potentially get logged by process monitoring tools, CI/CD systems, or crash reporters that capture command-line arguments.

The PR description also highlights a related concern: the XMEMO_KEY environment variable is the documented "safe" alternative, but even environment variables aren't fully safe from local process inspection — and the --allow-plaintext flag can direct the tool to write credentials unencrypted to ~/.xmemo/skill-credentials.json. Combined, these gaps meant an attacker with local system access (a compromised low-privilege account, a malicious process, or shared-host tenant) had multiple paths to recover secrets: read shell history, inspect /proc, or find the plaintext credentials file.

For a Node.js library like xmemo that's consumed by other developers and pipelines, this matters a lot — any downstream project building automation on top of xmemo-skill.mjs inherits this gap unless it's fixed at the source.

The Fix

The fix tightens the regex used in validateCommandInput() to catch significantly more credential-shaped flag names:

Before:

if (/^(token|api[-_]?key|bearer|authorization|cookie|secret)$/i.test(key) && key !== 'from-stdin') {
  throw new Error(`Refusing sensitive command-line option --${key}. Use XMEMO_KEY or --from-stdin where documented.`);
}

After:

if (/^(token|api[-_]?key|bearer|authorization|cookie|secret|password|passwd|credential|client[-_]?secret|refresh[-_]?token|access[-_]?token|private[-_]?key|xmemo[-_]?key)$/i.test(key) && key !== 'from-stdin') {
  throw new Error(`Refusing sensitive command-line option --${key}. Use XMEMO_KEY or --from-stdin where documented.`);
}

The new pattern adds password, passwd, credential, client[-_]?secret, refresh[-_]?token, access[-_]?token, private[-_]?key, and xmemo[-_]?key — all flag names that commonly carry secret material in real-world CLI tools. It preserves the existing from-stdin exception, so the documented safe path (piping a secret in via stdin) still works without being blocked.

This is a targeted, single-file change: it doesn't alter the behavior of any legitimate, non-credential flag, and it doesn't touch the command dispatch logic elsewhere in the file. Because the check runs early in validateCommandInput(), any of these newly-blocked flag names will now throw immediately with a clear error message pointing users toward the XMEMO_KEY environment variable or --from-stdin, rather than silently accepting a secret on the command line.

Key Takeaways

  • The credential-flag blocklist in validateCommandInput() (xmemo-skill.mjs:386) was missing common secret-related names like password, client-secret, access-token, and xmemo-key — a reminder that regex blocklists need continuous expansion as new naming conventions emerge.
  • Command-line arguments are never truly private on a multi-user or monitored system; ps aux and shell history can expose them long after the command finishes running.
  • The --allow-plaintext flag writing to ~/.xmemo/skill-credentials.json represents a separate, deliberate risk trade-off that should be treated as an explicit opt-in, not a convenience default.
  • Even the "safe" documented path — the XMEMO_KEY environment variable — carries residual risk if set via shell startup scripts, so documentation should steer users toward safer injection methods like --from-stdin.
  • A single-line regex change had outsized security impact here, showing how important it is to review the completeness of security-relevant pattern matches, not just their presence.

How Orbis AppSec Detected This

  • Source: CLI flags parsed from process.argv (e.g., --password, --client-secret, --access-token) passed into validateCommandInput()
  • Sink: Credential values flowing into logged/stored command context, shell history, and process listings once accepted as valid flags
  • Missing control: The sensitive-flag detection regex in validateCommandInput() didn't cover common credential keyword variants, allowing several secret-bearing flag names to bypass the check entirely
  • CWE: CWE-522 (Insufficiently Protected Credentials), related to CWE-214 (Invocation of Process Using Visible Sensitive Information)
  • Fix: Expanded the regex at xmemo-skill.mjs:386 to also match password, passwd, credential, client-secret, refresh-token, access-token, private-key, and xmemo-key, rejecting these flags with a clear error message

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

This fix is a small diff with a meaningful security payoff: expanding a single regex in skills/xmemo/scripts/xmemo-skill.mjs closes off several previously-unguarded paths for leaking credentials through CLI flags. It's a good illustration of why blocklist-style security checks demand regular scrutiny — attackers (and well-meaning but careless users) will find the flag names you didn't think to block. Teams building CLI tools that touch secrets should pair this kind of defensive validation with broader guidance: avoid CLI-passed secrets altogether where possible, harden plaintext credential storage options, and steer users toward safer injection mechanisms like piped stdin input.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #26

Related Articles

critical

redmine_drawio View Hook Inlines Base64 Redmine API Keys

The Redmine drawio plugin's body-bottom view listener embedded the logged-in user's Redmine REST API key into client-side JavaScript on every wiki and issue page, "protected" only by Base64 encoding of the reversed string. Any script on the page — or anyone with browser developer tools, a cached copy of the HTML, or a proxy log — could decode it in one line and act as that user through the Redmine REST API. The fix removes the embedded credential from the rendered page entirely; the plugin's dia

critical

Yandex Translate API Key Leaked via URL Query Parameter

The `translateYandex()` helper built its request URL by interpolating the caller-supplied API key directly into the query string, meaning every call leaked the credential into server access logs, proxy logs, and any Referer header sent by intermediaries. The fix switches the request from a GET with the key in the URL to a POST with the key in the request body via `URLSearchParams`, removing the credential from any URL-logging surface entirely.

critical

Google Generative AI Keys Exposed in Client-Side Fetch URLs

A client-side AI model discovery utility was embedding Google Generative AI API keys directly in fetch request URLs, making them visible to any user inspecting network traffic or browser DevTools. The fix moves the key from the URL query parameter to a secure HTTP header, eliminating exposure while maintaining API authentication.

critical

How credential leakage through console logging happens in JavaScript browser extensions and how to fix it

A browser extension's `src/background/credentials.js` printed the full Strava authentication cookie string — including a signed JWT and CloudFront-Signature values — straight into the extension console via `console.debug`. Anyone who could open DevTools on the background page (or any tooling that scraped the console) could copy a live session and impersonate the user. The fix replaces the credential payload in both log statements with `Boolean(credentials)` and strips a realistic-looking JWT out

critical

How Hardcoded Secrets Compromise Authentication in JavaScript and How to Fix It

A critical vulnerability in `Tool/QuantumultX/Rewrite/RRSP.js` exposed hardcoded API authentication credentials—a TOKEN and UMID device identifier—directly in source code. Anyone with repository access could extract these credentials to impersonate the legitimate user and gain full account access to the RRTV API service. The fix replaced hardcoded secrets with empty placeholders, forcing users to manually configure credentials through secure channels.

critical

How API Key Exposure in Request Bodies happens in React and how to fix it

The Chatbot component in gitforme was transmitting Azure OpenAI API keys inside JSON request bodies, causing them to be logged by servers, proxies, and middleware. By moving the apiKey from requestBody.apiKey to an Authorization header, credentials are now protected from persistence in generic request logging infrastructure.