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:
- Duplicated
<system> blocks appearing in the middle of user-content sections
- Corrupted prompt structure that can confuse model instruction hierarchy
- 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
- 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.
- Integration test: Use a real
.github/prompts/ template that {{#runtime-import}}s a file containing $` and verify no duplication occurs.
- 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
Bug:
String.prototype.replace()special replacement patterns corrupt rendered promptsContext
This is related to #29283
Problem
The JavaScript prompt rendering pipeline in
gh-awusesString.prototype.replace()to interpolate runtime content (from{{#runtime-import}}blocks and${{ github.event.* }}expressions) into prompt templates. However, JavaScript'sreplace()method treats certain character sequences in the replacement string as special patterns:$$$$&$`$'$nWhen 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:<system>blocks appearing in the middle of user-content sectionsgh-aw-threat-detectionbecause the renderedprompt.txtnow contains multiple<system>tagsReproduction
Given a prompt template:
And a
README.mdcontaining:The rendered output incorrectly becomes:
The
$`in the backtick-quoted regex caused everything before the match to be duplicated.Current State
runtime_import.cjs/interpolate_prompt.cjs(or equivalent) usesstr.replace(placeholder, content)wherecontentis untrusted file content from the repo being analyzed.$sequences in the replacement string.gh-aw-threat-detectiontool has to use fragile heuristics (looksLikeReplacementTokenExpansion) to distinguish this rendering bug from actual prompt injection attacks.Target State
replace().prompt.txtmust be a faithful representation of template + interpolated content with no structural corruption.Approach
Option A: Use a replacer function (Preferred)
Replace:
With:
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 stringsThis 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), and assert the rendered output matches the literal content..github/prompts/template that{{#runtime-import}}s a file containing$`and verify no duplication occurs.$patterns).Impact
gh-aw-threat-detectionlooksLikeReplacementTokenExpansionheuristic downstream