Skip to content

Fix #788: Fix quadratic ReDoS in cURL/Postman import placeholder regexes - #789

Merged
keysersoft merged 4 commits into
HelpCode-ai:mainfrom
pamod-madubashana:pamod-madubashana/issue-788-36407257745
Sep 30, 2026
Merged

keysersoft merged 4 commits into
HelpCode-ai:mainfrom
pamod-madubashana:pamod-madubashana/issue-788-36407257745

Conversation

@pamod-madubashana

Copy link
Copy Markdown

Fixes #788

I fixed the quadratic ReDoS in the cURL/Postman import placeholder regexes (#788).

What changed:

  • Replaced every loose {{([^}]+)}} shape with disjoint classes {{\s*([^{}\s]+)\s*}} so a brace run with no closing }} fails fast in linear time. This matches the pattern I already use in caller-context.util.ts:34.
  • Files: packages/backend/src/common/env-interpolation.util.ts, packages/backend/src/common/unresolved-placeholders.util.ts, packages/backend/src/connectors/connector-secrets.util.ts, packages/backend/src/connectors/catalog-env-rebuild.util.ts, packages/backend/src/connectors/parsers/curl.parser.ts, packages/backend/src/connectors/parsers/postman.parser.ts.
  • In curl.parser.ts I also removed the greedy .*{{...}}.* header extraction in favor of a single safe .match() for the variable name, and made the generateToolName strip patterns ({{...}} / {...}) use [^{}]+ with doubles stripped first.
  • In postman.parser.ts I applied the same treatment to the path-param, URL-sanitize, body-sentinel, and tool-name patterns.
  • Behavior for valid inputs is unchanged, including {{ SPACED }} (the surrounding \s* preserves the existing .trim() handling).

Why it addresses the issue:

  • The import endpoint runs CurlParser.parseUrl over user-supplied URLs with a 10mb body limit, so one 80KB paste of {{{{... stalled the event loop for seconds (4x input ~16x time). With the fixed shape the same 8K/32K/80K inputs scan in well under a millisecond, and the shared runtime interpolation/detection helpers get the same protection.

Verification

  • ReDoS perf check (fixed-shape replace over 'https://api.example.com/'+'{{{{'.repeat(k)): chars=8024 0.3ms / chars=32024 0.3ms / chars=80024 0.7ms — all under 5ms, linear.
  • Tests: npm run test --workspace=packages/backend on the 8 related suites (env-interpolation, unresolved-placeholders, caller-context, base-url-variable, connector-secrets, catalog-env-rebuild, curl.parser, postman.parser) — 8 suites passed, 257 tests passed, 0 failed.

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@keysersoft keysersoft left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, the repro in #788 is solid. I confirmed it on main: {{{{… at 32k chars already takes ~370 ms through the old {{([^}]+)}}, and your main pattern brings that down to ~0.1 ms.

One problem before I can merge though. The strip patterns you added, \{\{\s*[^{}]+\s*\}\} (curl.parser.ts line 320 in generateToolName, postman.parser.ts lines 181 and 290), open a worse ReDoS than the one they replace. \s is a subset of [^{}], so {{ followed by a run of whitespace with no closing }} gets split three ways between \s*, [^{}]+ and \s* and backtracks cubically.

Numbers from my machine:

  • bare regex, https://a/{{ + 8,000 spaces: 121 s (the old pattern does it in ~26 ms)
  • end to end, a Postman collection whose raw url is https://api.example.com/{{ + 2,000 spaces: 0 ms on main, 1.6 s on this branch, and it keeps growing with the cube of the length

So as it stands it trades one ReDoS for a bigger one on the same import endpoint.

What I'd suggest instead is one shape everywhere: \{\{([^{}]+)\}\}, then .trim() the capture where the name matters. [^{}] is disjoint from both braces, so it's linear on brace runs and on whitespace runs (I measured 0.0 to 0.1 ms on all three hostile inputs), and it keeps the current semantics. That second part matters in unresolved-placeholders.util.ts: with [^{}\s]+ a placeholder like {{ my var }} is no longer recognised, so the guard would let the literal go out to the vendor instead of failing closed.

Three smaller things:

  1. Please drop the package-lock.json change. Those libc removals come from your npm version, not from the fix.
  2. Could you add a spec with the hostile inputs (a brace run, and {{ plus a long whitespace run) for both parsers and for env-interpolation.util.ts, asserting each finishes quickly at ~50k chars? Otherwise the next regex tweak can bring this back without anyone noticing.
  3. The CLA check is red. You sign it by posting the sentence from the bot's comment as a PR comment exactly as written there, since the action matches the text character for character.

Once you push, I'll approve the CI run (fork PRs need that on our side).

@pamod-madubashana

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Sep 30, 2026
pamod-madubashana and others added 2 commits September 30, 2026 10:11
… everywhere

- Replace all {{\s*([^{}\s]+)\s*}} with {{([^{}]+)\}} for linear-time matching
- Add .trim() where variable names are captured
- Revert package-lock.json change
- Add ReDoS regression tests for env-interpolation, curl.parser, postman.parser
@pamod-madubashana

Copy link
Copy Markdown
Author

recheck

…, last loose placeholder regex

- package-lock.json back to main's version (the libc removals came from a
  different npm, not from the fix).
- The ReDoS specs allow 250 ms instead of 100: the fixed patterns take ~1 ms,
  the old ones take seconds at 50k chars, so the test still catches a
  regression without flaking on a slow CI runner.
- adapters.service.ts hasUsableValue() had the same /\{\{[^}]+\}\}/ shape:
  3.2 s on 50k '{' against 1 ms now.
@keysersoft
keysersoft requested a review from D3nisty as a code owner September 30, 2026 07:22
if (typeof url === 'string') {
try {
const parsed = new URL(url.replace(/\{\{[^}]+\}\}/g, 'placeholder'));
const parsed = new URL(url.replace(/\{\{[^{}]+\}\}/g, 'placeholder'));

@keysersoft keysersoft left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @pamod-madubashana, this is exactly what was needed: one linear shape everywhere, and the hostile-input specs to keep it that way.

I pushed one small follow-up commit on top so it could go in today:

  • package-lock.json back to main's version;
  • the timing bounds in the new specs raised from 100 to 250 ms, so they don't flake on a slow CI runner (the fixed patterns take ~1 ms, the old ones take seconds at 50k chars, so they still catch a regression);
  • the same fix for hasUsableValue() in adapters.service.ts, which still had /\{\{[^}]+\}\}/ (3.2 s on 50k { before, 1 ms now).

Merging.

@keysersoft
keysersoft merged commit deb218c into HelpCode-ai:main Sep 30, 2026
13 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix quadratic ReDoS in cURL/Postman import placeholder regexes

3 participants