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:
- Land in the shell's history file (
.bash_history,.zsh_history), often stored in plaintext indefinitely. - Be visible to any local user running
ps auxor/proc/<pid>/cmdlinewhile the process is executing. - 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.
Prevention & Best Practices
- Prefer allowlists over blocklists for security-sensitive input. A blocklist regex like this one requires ongoing maintenance to stay ahead of new naming conventions (
refresh-token,access-token, etc.). Where possible, define an explicit allowlist of accepted flags per command instead of trying to enumerate every dangerous name. - Never accept secrets via CLI arguments at all, if you can avoid it. Command-line arguments are visible in process listings (
ps aux,/proc/<pid>/cmdline) and often persisted to shell history. Environment variables read directly by the process, OS keychains, or secret managers are safer defaults. - Treat "plaintext fallback" flags like
--allow-plaintextas high-risk features. If your tool supports writing credentials to disk unencrypted, make it opt-in, loudly documented, and ideally gated behind additional confirmation or file permission hardening (e.g.,chmod 600). - Audit environment variable usage for secrets. Even
XMEMO_KEYisn't fully safe — document that users should avoid setting secrets in shell profile files (.bashrc,.zshrc) and instead use per-session exports or dedicated secret-injection tooling. - Use static analysis and secret-scanning tools (Semgrep, GitLeaks, or AI-assisted scanners like the one that caught this issue) to continuously check for credential-shaped CLI flags, hardcoded secrets, and insecure storage patterns.
- Reference the OWASP Secrets Management Cheat Sheet when designing how a CLI tool should accept and store credentials.
Key Takeaways
- The credential-flag blocklist in
validateCommandInput()(xmemo-skill.mjs:386) was missing common secret-related names likepassword,client-secret,access-token, andxmemo-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 auxand shell history can expose them long after the command finishes running. - The
--allow-plaintextflag writing to~/.xmemo/skill-credentials.jsonrepresents 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_KEYenvironment 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 intovalidateCommandInput() - 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:386to also matchpassword,passwd,credential,client-secret,refresh-token,access-token,private-key, andxmemo-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.