π΄ 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:
- 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
validate_patch_paths validates path secrets.txt β passes (valid filename)
- Stage 2 threat detection may or may not catch this
- Stage 3 applies the patch via
git am / git apply β creates symlink secrets.txt -> /proc/self/environ in the worktree
collect_changes_from_diff_tree / collect_changes_from_worktree: is_file("secrets.txt") follows symlink β /proc/self/environ is a regular file β returns true
read_file_change("add", "secrets.txt", path) β tokio::fs::read follows symlink β reads /proc/self/environ
- 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 Β· β·
π΄ Red Team Security Audit
Audit focus: Category B (Path Traversal & File System) β create-pr safe output
Severity: High
Findings
src/safeoutputs/create_pr.rs:1659,1665,1747,1782,src/safeoutputs/create_pr.rs:1793-1829Details
Finding 1: Symlink in create-pr patch can exfiltrate Stage 3 write tokens
Description: The
create-prsafe output allows an agent to submit a git patch that is applied to a fresh worktree in Stage 3. The path validation functionvalidate_patch_paths/validate_single_pathcorrectly 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 symlinkscollect_changes_from_worktree(line 1659, 1665): same patternread_file_changethen callstokio::fs::read(full_path)which also follows symlinks. The file contents (e.g., the Stage 3 environment containingSYSTEM_ACCESSTOKEN) are uploaded to ADO as the content of the "new file" in the PR.Attack vector:
validate_patch_pathsvalidates pathsecrets.txtβ passes (valid filename)git am/git applyβ creates symlinksecrets.txt -> /proc/self/environin the worktreecollect_changes_from_diff_tree/collect_changes_from_worktree:is_file("secrets.txt")follows symlink β/proc/self/environis a regular file β returnstrueread_file_change("add", "secrets.txt", path)βtokio::fs::readfollows symlink β reads/proc/self/environSYSTEM_ACCESSTOKEN(the write-capable token) β is uploaded to ADO as the content ofsecrets.txtin the PR branchProof of concept:
Stage 1 agent (in AWF sandbox) calls the
create-prMCP 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 (usingbashtools), whichgit add -Awould capture, resulting in a mode-120000 entry in the generatedformat-patchoutput.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 serverThe MCP server calls
git add -Aand then creates a synthetic commit before callinggit format-patch. The resulting patch contains the symlink.Impact:
SYSTEM_ACCESSTOKEN(write-capable ADO token) is exfiltrated to the ADO repository as PR file contentAZURE_DEVOPS_EXT_PAT, pipeline variables) are also exposedScope: Affects any pipeline with
create-prenabled insafe-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_treeandcollect_changes_from_worktree, replaceis_file()(which follows symlinks) with a symlink-aware check:This same fix should be applied at all seven
is_file()call sites incollect_changes_from_diff_treeandcollect_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_pathscould be extended to detectmode 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.rsin a previous patch, whereis_dir()was replaced withsymlink_metadata().file_type().is_symlink()checks to prevent directory-symlink following.Audit Coverage
This issue was created by the automated red team security auditor.