Skip to content

πŸ”΄ Red Team Audit β€” High: create-pr patch can contain symlink to exfiltrate Stage 3 write tokensΒ #546

@github-actions

Description

@github-actions

πŸ”΄ Red Team Security Audit

Audit focus: Category B (Path Traversal & File System) β€” create-pr safe output
Severity: High

Findings

# Vulnerability Severity File(s) Exploitable?
1 Symlink in agent patch exfiltrates Stage 3 executor environment High src/safeoutputs/create_pr.rs:1659,1665,1747,1782, src/safeoutputs/create_pr.rs:1793-1829 Yes

Details

Finding 1: Symlink in create-pr patch can exfiltrate Stage 3 write tokens

Description: The create-pr safe output allows an agent to submit a git patch that is applied to a fresh worktree in Stage 3. The path validation function validate_patch_paths / validate_single_path correctly rejects .. traversal and absolute paths, but only validates the symlink name β€” not the symlink target. An agent can craft a patch that creates a symlink to any file accessible to the Stage 3 executor (e.g. /proc/self/environ).

After applying the patch, two functions collect file changes to upload to ADO:

  • collect_changes_from_diff_tree (line 1747): if full_path.is_file() β€” is_file() follows symlinks
  • collect_changes_from_worktree (line 1659, 1665): same pattern

read_file_change then calls tokio::fs::read(full_path) which also follows symlinks. The file contents (e.g., the Stage 3 environment containing SYSTEM_ACCESSTOKEN) are uploaded to ADO as the content of the "new file" in the PR.

Attack vector:

  1. Agent creates a git format-patch file containing a new symlink entry:
    diff --git a/secrets.txt b/secrets.txt
    new file mode 120000
    --- /dev/null
    +++ b/secrets.txt
    @@ -0,0 +1 @@
    +/proc/self/environ
    \ No newline at end of file
    
  2. validate_patch_paths validates path secrets.txt β†’ passes (valid filename)
  3. Stage 2 threat detection may or may not catch this
  4. Stage 3 applies the patch via git am / git apply β†’ creates symlink secrets.txt -> /proc/self/environ in the worktree
  5. collect_changes_from_diff_tree / collect_changes_from_worktree: is_file("secrets.txt") follows symlink β†’ /proc/self/environ is a regular file β†’ returns true
  6. read_file_change("add", "secrets.txt", path) β†’ tokio::fs::read follows symlink β†’ reads /proc/self/environ
  7. The entire Stage 3 environment β€” including SYSTEM_ACCESSTOKEN (the write-capable token) β€” is uploaded to ADO as the content of secrets.txt in the PR branch

Proof of concept:

Stage 1 agent (in AWF sandbox) calls the create-pr MCP tool. The MCP server generates a synthetic commit from staged changes. However, the agent could also have pre-created a file in the worktree that is a symlink (using bash tools), which git add -A would capture, resulting in a mode-120000 entry in the generated format-patch output.

Alternatively, if the agent has bash access to the worktree, it can:

ln -s /proc/self/environ /workspace/repo/secrets.txt
# ... git add -A is called by the MCP server

The MCP server calls git add -A and then creates a synthetic commit before calling git format-patch. The resulting patch contains the symlink.

Impact:

  • SYSTEM_ACCESSTOKEN (write-capable ADO token) is exfiltrated to the ADO repository as PR file content
  • Any other secrets in the Stage 3 environment (AZURE_DEVOPS_EXT_PAT, pipeline variables) are also exposed
  • The exfiltrated content appears as a file in the PR diff β€” visible to the Stage 3 executor itself (it uploaded it) and to any human reviewer or subsequent agent run that reads PR file diffs

Scope: Affects any pipeline with create-pr enabled in safe-outputs:. The symlink can target any file readable by the pipeline agent user, not just /proc/self/environ.

Note on Stage 2 mitigation: The threat detection stage is designed to review safe outputs, but relying on an AI agent to catch every symlink exfiltration attempt is not sufficient defense. The fix must be in the Stage 3 code path.

Suggested fix:

In collect_changes_from_diff_tree and collect_changes_from_worktree, replace is_file() (which follows symlinks) with a symlink-aware check:

// Before (follows symlinks β€” vulnerable):
if full_path.is_file() {
    changes.push(read_file_change("add", file_path, &full_path).await?);
}

// After (uses lstat β€” does not follow symlinks):
match tokio::fs::symlink_metadata(&full_path).await {
    Ok(meta) if meta.file_type().is_file() => {
        changes.push(read_file_change("add", file_path, &full_path).await?);
    }
    Ok(meta) if meta.file_type().is_symlink() => {
        // Skip symlinks β€” they would follow to arbitrary filesystem paths
        warn!("Skipping symlink in worktree: {} (symlink-following attack prevention)", file_path);
    }
    _ => {} // missing file or other type β€” skip
}

This same fix should be applied at all seven is_file() call sites in collect_changes_from_diff_tree and collect_changes_from_worktree:

  • create_pr.rs:1659, 1665, 1690, 1698 (worktree collector)
  • create_pr.rs:1747, 1768, 1777, 1782 (diff-tree collector)

Additionally, validate_patch_paths could be extended to detect mode 120000 (symlink mode) entries and reject them outright, as a belt-and-suspenders defense.

Prior art: A similar symlink-following fix was applied to src/tools/cache_memory/execute.rs in a previous patch, where is_dir() was replaced with symlink_metadata().file_type().is_symlink() checks to prevent directory-symlink following.


Audit Coverage

Category Status
A: Input Sanitization βœ… Scanned
B: Path Traversal βœ… Scanned (this finding)
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 Β· ● 9.3M Β· β—·

Metadata

Metadata

Labels

No labels
No 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