Skip to content

fix: avoid ReDoS when stripping fences from tool arguments (TypeScript, C#, Java, spec §9.8) - #585

Open
Arfath Ahmed Syed (ArfiArfath21) wants to merge 1 commit into
microsoft:mainfrom
ArfiArfath21:fix/fence-redos-ts-cs-java
Open

Arfath Ahmed Syed (ArfiArfath21) wants to merge 1 commit into
microsoft:mainfrom
ArfiArfath21:fix/fence-redos-ts-cs-java

Conversation

@ArfiArfath21

Copy link
Copy Markdown

Refs #581 (its table lists TypeScript, Java and C# as vulnerable; #582 fixes Python only, so this PR does not close it).

The fence-stripping regex from spec §9.8 is copied into the TypeScript, Java and C# runtimes:

^\s*```(?:json)?\s*\n?(.*?)\n?\s*```\s*$

\s*, \n? and the lazy group can all match the same whitespace, so an opening fence that never closes makes the engine try every split of that whitespace, which is cubic. The input is the model's tool-call arguments, so anything that can steer the model can stall the agent loop.

Measured on the TypeScript regex from main ("```json" + " " * n + "x"): 16 ms at n=200, 262 ms at 800, 1,302 ms at 1,600; it takes minutes at 10,000. #581 has the same numbers for Python. I could not run the Java and C# builds here (see below), but their patterns are character-for-character the same.

What changed:

  • runtime/typescript/.../core/tool-dispatch.ts, runtime/csharp/Prompty.Core/ToolDispatch.cs, runtime/java/.../ToolDispatch.java: a small stripCodeFence helper replaces the regex. It trims the whitespace, requires an opening and closing fence (at least 6 characters, so the two can't overlap), drops an optional json tag and trims again. Strategy order, warnings and fall-through to the block extractor are unchanged. The Java helper uses the ASCII whitespace set that Java's \s matches, so Unicode spaces behave as before.
  • spec/spec.md §9.8 and web/.../specification/agent-loop.mdx: Strategy 2 now calls strip_code_fence, defined with string operations, with a note on why the regex form must not be used. Without this, anyone implementing the spec literally gets the same bug. Rust and Go use linear-time regex engines and are not affected.
  • Tests in each runtime: fences with surrounding whitespace, fences without a language tag, an unclosed fence falling through to block extraction, empty fences returning an error, and an unclosed fence followed by 20,000 spaces or newlines finishing in under a second.

Verification:

  • TypeScript: vitest in packages/core 1,237 passed, tsc --noEmit and format:check clean. The slow-input test hangs on the old regex (I stopped it after 150 s) and passes in milliseconds now.
  • Equivalence: compared the old regex with the new function on 400,000 random fence-heavy strings in JavaScript (Unicode whitespace and BOM included) and on 300,000 for the Java algorithm against an ASCII-whitespace regex, with no differences in what gets passed to the JSON parser.
  • Not run: the C# and Java test suites. This machine has no .NET SDK or JDK, so those two edits and their tests are unbuilt; CI will be their first compile. The code is a direct translation of the TypeScript helper, and I kept the tests in the style of the existing ones in ResilienceTests.cs and RuntimeTest.java. If anything fails to compile I will fix it right away.

…eScript, C# and Java runtimes

The fence-stripping regex copied from spec section 9.8 backtracks cubically
when tool-call arguments open a fence that never closes. The arguments string
is model output, so a prompt injection can stall the agent loop.

Replace it with plain string operations that return the same text, in
TypeScript, C# and Java, and change the spec pseudocode so new implementations
do not copy the regex. Python is covered by microsoft#582; Rust and Go use linear-time
regex engines.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant