Skip to content

Bug: String.prototype.replace() special replacement patterns corrupt rendered prompts #30449

Description

@davidslater

Bug: String.prototype.replace() special replacement patterns corrupt rendered prompts

Context

This is related to #29283

Problem

The JavaScript prompt rendering pipeline in gh-aw uses String.prototype.replace() to interpolate runtime content (from {{#runtime-import}} blocks and ${{ github.event.* }} expressions) into prompt templates. However, JavaScript's replace() method treats certain character sequences in the replacement string as special patterns:

Pattern Behavior
$$ Inserts a literal $
$& Inserts the matched substring
$` Inserts the portion of the string before the match
$' Inserts the portion of the string after the match
$n Inserts the nth capture group

When untrusted content from the target repository (e.g., file contents pulled in via {{#runtime-import}}) contains $`, the replacement engine splices everything before the match point — including the <system> block — into the rendered output. This results in:

  1. Duplicated <system> blocks appearing in the middle of user-content sections
  2. Corrupted prompt structure that can confuse model instruction hierarchy
  3. False positive threat detections in gh-aw-threat-detection because the rendered prompt.txt now contains multiple <system> tags

Reproduction

Given a prompt template:

<system>
You are a helpful assistant.
</system>

Here is the file content:
{{#runtime-import src="README.md"}}

And a README.md containing:

Use regex like `^\s*<system>` to match system blocks.

The rendered output incorrectly becomes:

<system>
You are a helpful assistant.
</system>

Here is the file content:
<system>
You are a helpful assistant.
</system>

Here is the file content:
Use regex like `^\s*<system>` to match system blocks.

The $` in the backtick-quoted regex caused everything before the match to be duplicated.

Current State

  • runtime_import.cjs / interpolate_prompt.cjs (or equivalent) uses str.replace(placeholder, content) where content is untrusted file content from the repo being analyzed.
  • No escaping of special $ sequences in the replacement string.
  • The downstream gh-aw-threat-detection tool has to use fragile heuristics (looksLikeReplacementTokenExpansion) to distinguish this rendering bug from actual prompt injection attacks.

Target State

  • All interpolation calls must neutralize special replacement patterns in the content string before passing it to replace().
  • The rendered prompt.txt must be a faithful representation of template + interpolated content with no structural corruption.

Approach

Option A: Use a replacer function (Preferred)

Replace:

rendered = template.replace(placeholder, content);

With:

rendered = template.replace(placeholder, () => content);

When a function is passed as the second argument to replace(), no special pattern substitution occurs — the return value is used as-is.

Option B: Escape $ characters in replacement strings

function escapeReplacement(str) {
  return str.replace(/\$/g, '$$$$');
}
rendered = template.replace(placeholder, escapeReplacement(content));

This doubles every $ so that $$ → literal $, neutralizing all special patterns.

Recommendation

Option A is simpler, more robust, and has no risk of double-escaping. Apply it to every .replace() call where the replacement string contains untrusted (runtime-imported or user-provided) content.

Testing

  1. Unit test: Create a prompt template with a placeholder, provide content containing each special pattern ($`, $&, $', $1), and assert the rendered output matches the literal content.
  2. Integration test: Use a real .github/prompts/ template that {{#runtime-import}}s a file containing $` and verify no duplication occurs.
  3. Regression test: Verify the existing test suite still passes (no behavioral change for content without $ patterns).

Impact

  • Eliminates a class of false-positive prompt injection detections in gh-aw-threat-detection
  • Prevents prompt structure corruption that could affect model behavior
  • Removes the need for the looksLikeReplacementTokenExpansion heuristic downstream

Activity

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions