PHP2Shell: Read our blog on the critical RCE vulnerabilities affecting 70% of the Internet
Vulnerability DatabaseGHSA-5cjr-rc2p-f66f

GHSA-5cjr-rc2p-f66f: 
JavaScript vulnerability analysis and mitigation

Description

The never-allowed rules in the shipped classifier are regular expressions matched against literal command text. Two consequences follow, and both are observable by running the published npm artifact's own classifyCommand() and PolicyEngine:

  1. The catastrophic rm rule does not tolerate flag reordering, separated flags, or long-flag spellings. The shipped pattern is /rm\s+-rf?\s+\/(\s|$)/, which matches only the exact spelling rm -rf / (or rm -r /). The same operation spelled any other way is not covered.
  2. The respelled form is not merely unlisted — it is classified safe, and safe is the one class that the ask-destructive approval policy never prompts on. The result is that under the shipped default approval policy, a set of destructive commands executes with no approval prompt at all, while the equivalent command in the canonical spelling is refused outright. This is not a new class for this project: the maintainers' own published advisory record documents this same class repeatedly (for example GHSA-qvx5-rxrj-9vfh, Critical, CVSS 9.9, and GHSA-cm4c-4q74-mgmj). This report is one more instance of it, found by executing the current release.

Steps to reproduce

Environment: ssh-mcp@2.20.1 from the public npm registry, Node 22. Profile: role = "operator", group = "prod", approvalPolicy = "ask-destructive" (the shipped default). No SSH connection is needed — the shipped build/policy/classifier.js and build/policy/engine.js decide locally.

import { classifyCommand } from './build/policy/classifier.js';
import { PolicyEngine } from './build/policy/engine.js';
const engine = new PolicyEngine();
const profile = {
  name: 'p', role: 'operator', hostGroup: 'prod', group: 'prod',
  approvalPolicy: 'ask-destructive',
};
for (const cmd of [
  'rm -rf /', 'rm -fr /', 'rm -r -f /', 'rm --recursive --force /', 'rm -R -f /',
  'mkfs.ext4 /dev/sda1', 'mkfs -t ext4 /dev/sda1',
]) {
  const r = engine.evaluate(cmd, profile);
  console.log(cmd.padEnd(28), r.commandClass.padEnd(12), r.decision, r.ruleId);
}

Observed output (2.20.1; identical on 2.20.0, whose build/policy/ is byte-for-byte the same):

rm -rf /                  destructive  deny            denylist
rm -fr /                  safe         allow          default
rm -r -f /                safe         allow          default
rm --recursive --force /  safe         allow          default
rm -R -f /                safe         allow          default
mkfs.ext4 /dev/sda1       destructive  deny            denylist
mkfs -t ext4 /dev/sda1    safe         allow          default

The allow / default rows are the finding: allow with ruleId: "default" means the command passed every gate — the denylist did not match, the role permitted its class, and the approval policy did not consider it approval-worthy. ask-destructive prompts only on destructive and privileged, so a safe classification is silent. Switching only the approval policy to ask-all returns require-approval for every one of the respelled rows, because ask-all gates every class. That is the mitigation, and it is the same mitigation the project's own SECURITY.md already recommends for any profile that matters.

Affected versions

Observed on ssh-mcp@2.20.1 and ssh-mcp@2.20.0. The /rm\s+-rf?\s+\/(\s|$)/ never-allowed pattern is byte-identical in 2.19.2 and 2.15.0 as well, and ask-destructive has shipped as the default approvalMode throughout, so the affected range is at least < 2.20.2 — I did not bisect further back, and am leaving the lower bound to the maintainer.

Potential impact

Under the default policy, an agent that proposes one of these spellings executes it on the target host with no prompt, so the operator's approval veto is never invoked for an operation the tool's own never-allowed list exists to prevent. The blast radius is bounded by the privileges of the profile's remote account, which is the same reason the project has said in its threat model that this class of gap "is not fixable by classifying harder" — a bypassed classifier only yields what the account can already do. Two further observations that I believe are more consequential than the spelling itself, and which I would ask the maintainer to weigh:

  • The safe class is broader than the name suggests. Executing classifyCommand() shows systemctl stop nginx, systemctl restart nginx, apt-get install -y nginx, docker restart myapp, rm -f /etc/passwd, useradd evil, usermod -aG sudo evil, chmod 777 /etc/shadow, and a NOPASSWD:ALL sudoers append all classify as safe rather than destructive. Under ask-destructive these are also silent. This is documented in SECURITY.md as intended behaviour ("ask-destructive ... does not buy one on writes, deletions spelled another way, service control, or signals"), so by the project's own stated standard in .oss-scanner/threat_model.md — "behaviour matching that description is not a finding; behaviour that is worse than it describes is" — the service-control and privilege-escalation rows are the ones that are worse than described, because the README presents rm -rf / and mkfs as never allowed "whatever the role or approval policy", which a reader would reasonably take to cover the operation rather than one spelling.
  • rm -R -f / (uppercase R) and rm -Rf / are also uncovered, and -R is a valid recursive flag for rm.

Suggested mitigations

  1. Make the never-allowed rm rule tolerant of flag order, separated flags, long-flag spellings, and the uppercase -R form — i.e. match the operation rather than one spelling of it.
  2. Consider whether a command that resolves to a catastrophic target should be classified destructive even when the spelling is not recognised, so that ask-destructive prompts rather than staying silent. Failing that, the pattern-matching gap is a strong additional argument for ask-all on any profile that points at a machine that matters — which the documentation already says.
  3. Consider classifying privilege-escalation commands (useradd, usermod -aG sudo, writes to /etc/sudoers, chmod 777 /etc/shadow) as privileged rather than safe, so elevation is gated under every approval policy rather than only under ask-all. I am happy to share the exact reproduction script, and to test a fix against it once one is available. I have not published anything about this publicly, and will not do so before the 90-day window in SECURITY.md expires or a fixed release is published, whichever comes first. Please credit me as you prefer. One note on the class of this report: I am not claiming a CVE identifier, and I have not filed this anywhere other than this private advisory.

Source: NVD

Related JavaScript vulnerabilities:

CVE ID

Severity

Score

Technologies

Component name

CISA KEV exploit

Has fix

Published date

GHSA-5cjr-rc2p-f66fHIGH8.8
  • JavaScript logoJavaScript
  • ssh-mcp
NoYesOct 23, 2026
GHSA-5www-2x9c-mgr6HIGH8.8
  • JavaScript logoJavaScript
  • @pineforge/codegen-pyodide
NoYesOct 23, 2026
CVE-2026-58497HIGH8.6
  • JavaScript logoJavaScript
  • @zenalexa/unicli
NoYesOct 23, 2026
CVE-2026-115477HIGH7.1
  • JavaScript logoJavaScript
  • obsidian-tc
NoYesOct 23, 2026
GHSA-2g9m-gh52-f39vHIGHN/A
  • JavaScript logoJavaScript
  • koa-static-resolver
NoYesOct 23, 2026

Free Vulnerability Assessment

Benchmark your Cloud Security Posture

Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.

Request assessment

Get a personalized demo

Ready to see Wiz in action?

"Best User Experience I have ever seen, provides full visibility to cloud workloads."
David EstlickCISO
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
Adam FletcherChief Security Officer
"We know that if Wiz identifies something as critical, it actually is."
Greg PoniatowskiHead of Threat and Vulnerability Management