π΄ Red Team Security Audit
Audit focus: Category A (Input Sanitization & Injection) β Stage 3 safe output executors
Severity: High
Findings
| # |
Vulnerability |
Severity |
File(s) |
Exploitable? |
| 1 |
VSO injection via file_path in upload success messages |
High |
src/safeoutputs/upload_workitem_attachment.rs:334-338, src/safeoutputs/upload_build_attachment.rs:540-544, src/safeoutputs/upload_pipeline_artifact.rs:614-618 |
Yes |
| 2 |
Ironic double-injection: the ##vso[ content guard's own failure message injects via filename |
High |
src/safeoutputs/upload_workitem_attachment.rs:225-228 |
Yes |
Details
Finding 1 & 2: file_path in Stage 3 upload messages printed unsanitized to stdout
Description: All three upload safe-output tools (upload-workitem-attachment, upload-build-attachment, upload-pipeline-artifact) include the agent-supplied file_path field verbatim in ExecutionResult::success / ExecutionResult::failure messages. These messages are printed to stdout by execute.rs:217:
println!("[{}/{}] {} - {} - {}", i + 1, total, tool_name, symbol, result.message);
The log_and_print_entry_result function applies no neutralization to result.message in the Ok(ExecutionResult) arm. Neutralization only happens in the Err(e) arm (hard errors from ? propagation).
Root cause: file_path validation (validate()) in all three tools blocks .., absolute paths, :, null bytes, newlines, and .git components β but does not block ##vso[ or ##[ sequences. sanitize_content_fields() for these tools does not include file_path.
Vulnerable code paths:
upload_workitem_attachment.rs success path (line 334β338):
Ok(ExecutionResult::success_with_data(
format!(
"Uploaded '{}' and linked to work item #{}",
filename, self.work_item_id // β filename derived from self.file_path
),
...
))
upload_workitem_attachment.rs content guard failure (line 225β228) β the check designed to prevent injection causes injection via the filename:
return Ok(ExecutionResult::failure(format!(
"File '{}' contains '##vso[' command injection sequence",
self.file_path // β agent-controlled, printed without neutralization
)));
upload_build_attachment.rs success path (line 540β544):
Ok(ExecutionResult::success_with_data(
format!(
"Attached '{}' to build #{} as artifact '{}'",
self.file_path, effective_build_id, final_name // β unsanitized
),
...
))
upload_pipeline_artifact.rs success path (line 614β618):
Ok(ExecutionResult::success_with_data(
format!(
"Published '{}' as pipeline artifact '{}' on build #{}",
self.file_path, final_name, effective_build_id // β unsanitized
),
...
))
```
**Attack vector**:
1. In Stage 1, the agent's bash step creates a file with an ADO logging command embedded in the name:
```bash
touch '##vso[task.setvariable variable=EXPLOIT]value.txt'
```
2. The agent calls the `upload-workitem-attachment` (or `upload-build-attachment` / `upload-pipeline-artifact`) MCP tool with:
```json
{ "file_path": "##vso[task.setvariable variable=EXPLOIT]value.txt", "work_item_id": 123 }
```
3. `validate()` passes: no `..`, no absolute path, no `:`, no null bytes, no newlines, no `.git`.
4. At Stage 3, `canonicalize()` succeeds (file exists), `starts_with(source_dir)` passes.
5. The success message `"Uploaded '##vso[task.setvariable variable=EXPLOIT]value.txt' and linked to work item #123"` is printed to stdout.
6. Azure DevOps interprets `##vso[task.setvariable variable=EXPLOIT]` as a pipeline logging command.
**Alternative exploitation path** (upload-workitem-attachment only β note irony): if the file content contains `##vso[`, the content guard returns a failure message that itself contains the malicious filename unsanitized:
```
File '##vso[task.setvariable variable=EXPLOIT]evil.txt' contains '##vso[' command injection sequence
ADO parses the first ##vso[...] occurrence in this output and executes it.
Impact:
- Pipeline variable manipulation:
##vso[task.setvariable variable=X]value sets pipeline variables that persist for the duration of the Stage 3 job and affect any subsequent steps.
- Task completion override:
##vso[task.complete result=Succeeded] can mark the Stage 3 task as succeeded even if subsequent write operations fail, hiding failures from the pipeline summary.
- Build tagging:
##vso[build.addbuildtag tag=X] adds build tags that may affect release gate conditions.
- Log injection:
##vso[task.logissue type=error;...]message creates spurious error annotations visible in the ADO build results UI.
Suggested fix:
-
Primary fix: Apply neutralize_pipeline_commands to result.message in log_and_print_entry_result before the println! call:
let safe_msg = neutralize_pipeline_commands(&result.message);
println!("[{}/{}] {} - {} - {}", i + 1, total, tool_name, symbol, safe_msg);
-
Belt-and-suspenders: Add ##vso[ / ##[ blocking to the file_path validation in all three upload tools, and/or include file_path in sanitize_content_fields().
-
Upload-workitem-attachment content guard fix: use a safe local variable for the failure message instead of embedding self.file_path:
return Ok(ExecutionResult::failure(
"Uploaded file contains '##vso[' command injection sequence β upload rejected".to_string()
));
Audit Coverage
| Category |
Status |
| A: Input Sanitization |
β
Scanned |
| 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 Β· β 7.2M Β· β·
π΄ Red Team Security Audit
Audit focus: Category A (Input Sanitization & Injection) β Stage 3 safe output executors
Severity: High
Findings
file_pathin upload success messagessrc/safeoutputs/upload_workitem_attachment.rs:334-338,src/safeoutputs/upload_build_attachment.rs:540-544,src/safeoutputs/upload_pipeline_artifact.rs:614-618##vso[content guard's own failure message injects via filenamesrc/safeoutputs/upload_workitem_attachment.rs:225-228Details
Finding 1 & 2:
file_pathin Stage 3 upload messages printed unsanitized to stdoutDescription: All three upload safe-output tools (
upload-workitem-attachment,upload-build-attachment,upload-pipeline-artifact) include the agent-suppliedfile_pathfield verbatim inExecutionResult::success/ExecutionResult::failuremessages. These messages are printed to stdout byexecute.rs:217:The
log_and_print_entry_resultfunction applies no neutralization toresult.messagein theOk(ExecutionResult)arm. Neutralization only happens in theErr(e)arm (hard errors from?propagation).Root cause:
file_pathvalidation (validate()) in all three tools blocks.., absolute paths,:, null bytes, newlines, and.gitcomponents β but does not block##vso[or##[sequences.sanitize_content_fields()for these tools does not includefile_path.Vulnerable code paths:
upload_workitem_attachment.rssuccess path (line 334β338):upload_workitem_attachment.rscontent guard failure (line 225β228) β the check designed to prevent injection causes injection via the filename:upload_build_attachment.rssuccess path (line 540β544):upload_pipeline_artifact.rssuccess path (line 614β618):ADO parses the first
##vso[...]occurrence in this output and executes it.Impact:
##vso[task.setvariable variable=X]valuesets pipeline variables that persist for the duration of the Stage 3 job and affect any subsequent steps.##vso[task.complete result=Succeeded]can mark the Stage 3 task as succeeded even if subsequent write operations fail, hiding failures from the pipeline summary.##vso[build.addbuildtag tag=X]adds build tags that may affect release gate conditions.##vso[task.logissue type=error;...]messagecreates spurious error annotations visible in the ADO build results UI.Suggested fix:
Primary fix: Apply
neutralize_pipeline_commandstoresult.messageinlog_and_print_entry_resultbefore theprintln!call:Belt-and-suspenders: Add
##vso[/##[blocking to thefile_pathvalidation in all three upload tools, and/or includefile_pathinsanitize_content_fields().Upload-workitem-attachment content guard fix: use a safe local variable for the failure message instead of embedding
self.file_path:Audit Coverage
This issue was created by the automated red team security auditor.