GHSA-GM3R-Q2WP-HW87: Root-cause fix for Shescape flag-protection DoS
Summary
The patch addresses the reported quadratic-time denial of service in Shescape's flag-protection path by removing repeated array destructuring and repeated join operations inside the processing loop in src/internal/compose.js. The new implementation computes fragments once, scans them by index, and performs a single slice().join() after the stopping point, which is consistent with eliminating the O(N^2) behavior described in the advisory. The same changeset also includes multiple shell-escaping correctness fixes for CMD, Dash, and Zsh, with corresponding fixture updates.
Analysis
Vulnerability
GHSA-GM3R-Q2WP-HW87 describes an algorithmic complexity denial of service in Shescape's default flag-protection logic. The vulnerable path repeatedly rebuilt the remaining argument string during iteration. In src/internal/compose.js, the old implementation destructured the fragment list and executed rest.join("") on each loop iteration while advancing through flag-like prefixes. Because both the array tail and the concatenated string were recreated repeatedly, attacker-controlled long inputs could trigger quadratic work.
let [preFlag, , ...rest] = flagFn(arg);
while (rest.length > 0 && escapeFn(preFlag) === "") {
arg = rest.join("");
[preFlag, , ...rest] = rest;
}This is source-grounded by the patch in commit 43d70b5 and the associated pull requests #2649 and #2651. The advisory scope is specifically the default flag-protection processing loop, not the unrelated escaping correctness issues bundled into the same release.
Patch
The patch rewrites the flag-protection loop to compute fragments once, iterate by index over alternating pre-flag fragments, and defer concatenation until the final starting offset is known. This removes the repeated join() and repeated tail copying from the hot path.
const fragments = flagFn(arg);
let idx = 0;
for (; idx < fragments.length - 2; idx += 2) {
const escapedFragment = escapeFn(fragments[idx]);
if (escapedFragment !== "") {
break;
}
}
arg = fragments.slice(idx).join("");That change appears in commit 43d70b5 and commit b4b34c3. The tests were updated so the compose unit test now asserts that escapeFn is called with the fragment returned by flagFn, matching the new control flow. The same patch train also adjusts shell-specific escaping rules in src/internal/win/cmd.js, src/internal/unix/dash.js, and src/internal/unix/zsh.js, and expands fixtures accordingly.
Review
Pros
- The complexity fix directly targets the root cause: repeated reconstruction of progressively smaller arrays and strings inside the loop in
src/internal/compose.js. - The new algorithm performs one
flagFn(arg)call, a linear scan over fragment positions, and one finalslice(idx).join(""), which is consistent with reducing the vulnerable path from quadratic to linear-time behavior for this stage. - The patch preserves the original semantic checkpoint of stopping when
escapeFn(preFlag)becomes non-empty, but does so without mutatingargon every iteration. - Tests were updated to reflect the new data flow, reducing the risk that the optimization silently diverges from intended compose behavior.
- The changelog explicitly records the optimization, making the security-relevant behavior change discoverable for downstream maintainers.
Cons
- The security fix is bundled with multiple escaping correctness changes for CMD, Dash, and Zsh. That increases review surface and makes it harder to isolate regression risk for the DoS remediation itself.
- The provided snippets do not include dedicated performance regression tests or adversarial-size benchmarks, so validation of the complexity improvement is inferential from code structure rather than demonstrated by automated measurement.
- The final
fragments.slice(idx).join("")still allocates a suffix array and output string once. That is acceptable for linear behavior, but it is not a zero-copy design. - The patch summary shows duplicated fixture blocks in the digest output, which does not imply a code defect but does make external review noisier.
Verdict
Root-cause.
The vulnerable behavior was the loop structure in src/internal/compose.js that repeatedly copied and rejoined the remaining fragments. The patch removes that pattern and replaces it with a single-pass index scan plus one final reconstruction, which is the correct structural remedy for the reported O(N^2) denial of service. Based on the advisory GHSA-GM3R-Q2WP-HW87 and the implementation changes in PR #2649, PR #2651, and commit 43d70b5, this is a substantive fix rather than a rate-limit, input cap, or superficial guard.
Recommended Labs
Try this vulnerability pattern yourself with hands-on labs.
- No Sutpo.js
Closest JavaScript hands-on defensive lab for this Shescape issue’s denial-of-service theme. It focuses on lack of rate limiting and uncontrolled resource consumption (CWE-770/CWE-400, OWASP A05:2021), which maps well to mitigating application-layer DoS even though the GHSA is specifically algorithmic complexity rather than request throttling.
- Energy.js
Relevant JavaScript defensive lab covering missing throttling and resource-consumption controls. Useful as a practical follow-on for learning how to harden endpoints against abusive workloads when a library bug can amplify CPU cost per request.
- Unparser.js
Best available JavaScript lab for algorithmic-complexity style DoS. It targets ReDoS (CWE-1333), another form of input-driven CPU exhaustion. While not the same root cause as the Shescape O(N^2) loop, it gives hands-on practice recognizing and fixing code paths where crafted input triggers disproportionate processing cost.