CVE Patch Review

GHSA-GM3R-Q2WP-HW87: Root-cause fix for Shescape flag-protection DoS

GHSA-GM3R-Q2WP-HW87 · Updated 2026-07-25 Root-cause

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 final slice(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 mutating arg on 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.

Sources