Skip to content

πŸ”΄ Red Team Audit β€” High: VSO command injection via untrusted ADO work item data in Stage 3 executorΒ #369

@github-actions

Description

@github-actions

πŸ”΄ Red Team Security Audit

Audit focus: Category E (Logic & Authorization Flaws) β€” post-refactor review of PR #368
Severity: High

Findings

# Vulnerability Severity File(s) Exploitable?
1 ##vso[ injection via work item title in prefix-guard error message High src/safeoutputs/update_work_item.rs:277-279 Yes β€” requires ADO work item write access
2 ##vso[ injection via work item tags in prefix-guard error message High src/safeoutputs/update_work_item.rs:296-298 Yes β€” requires ADO work item write access

Details

Finding 1 & 2: VSO Command Injection via Unsanitized ADO Data in Stage 3 println!

Description: The check_prefix_guards() function introduced in PR #368 fetches a work item from ADO and reads System.Title and System.Tags to enforce title-prefix / tag-prefix guards. When the guard fails, it embeds the fetched title/tags verbatim into an ExecutionResult::failure() message. That message is then printed to stdout via println! in the Stage 3 executor (execute.rs:200). Azure DevOps interprets ##vso[...] sequences in pipeline stdout as logging commands.

Vulnerable code (src/safeoutputs/update_work_item.rs):

// Line 271-279: current_title comes from ADO API β€” not sanitized
let current_title = wi.get("fields")
    .and_then(|f| f.get("System.Title"))
    .and_then(|t| t.as_str())
    .unwrap_or("");
if !current_title.starts_with(prefix.as_str()) {
    return Ok(Some(ExecutionResult::failure(format!(
        "Work item #{id} title '{current_title}' does not start with the required prefix..."
        //                      ^^^^^^^^^^^^^^ unsanitized ADO data in println! output
    ))));
}

// Line 286-298: raw_tags comes from ADO API β€” not sanitized
let raw_tags = wi.get("fields")
    .and_then(|f| f.get("System.Tags"))
    .and_then(|t| t.as_str())
    .unwrap_or("");
if !has_matching_tag {
    return Ok(Some(ExecutionResult::failure(format!(
        "...Current tags: '{raw_tags}'"
        //                 ^^^^^^^^ unsanitized ADO data in println! output
    ))));
}

Stage 3 output path (src/execute.rs:200):

println!("[{}/{}] {} - {} - {}", i + 1, total, tool_name, symbol, result.message);
//                                                                  ^^^^^^^^^^^^^^ printed to stdout; ADO interprets ##vso[
```

**Attack vector**:

1. Attacker has write access to at least one ADO work item in the target project (typical for any project member).
2. Attacker sets the work item title to a VSO logging command payload, e.g.:
   ```
   ##vso[task.setvariable variable=SC_WRITE_TOKEN;issecret=false]attacker_logs_this
   ```
3. Operator has configured `update-work-item` with `title-prefix: "[bot]"` (or any non-matching prefix).
4. An agent proposes `update-work-item` targeting that work item (the agent reads it from context β€” e.g., it was passed in as a trigger).
5. Stage 3 executes: `check_prefix_guards` fetches the work item, title doesn't match `[bot]`, and **calls** `ExecutionResult::failure(...)` with the raw title embedded.
6. `println!` outputs to ADO pipeline stdout:
   ```
   [1/1] update-work-item - βœ— - Work item #42 title '##vso[task.setvariable variable=SC_WRITE_TOKEN;issecret=false]...' does not start with...
   ```
7. ADO interprets the `##vso[task.setvariable]` command and re-exposes the `SC_WRITE_TOKEN` as a non-secret variable, leaking it to pipeline logs.

**Impact**:
- **Secret variable exfiltration**: `##vso[task.setvariable variable=X;issecret=false]` re-marks a secret as non-secret, causing it to appear in logs.
- **Environment manipulation**: `##vso[task.prependpath]attacker-path` injects a malicious directory into `PATH` for subsequent steps.
- **Artifact smuggling**: `##vso[artifact.upload]` can upload attacker-controlled data as a pipeline artifact.
- This bypasses the AWF sandboxing intent: Stage 1 is read-only, but an attacker controlling a work item can influence Stage 3 via this channel.

**Proof of concept** (malicious work item title):
```
##vso[task.setvariable variable=SC_WRITE_TOKEN;issecret=false]token_now_visible_in_logs

Configure the pipeline with title-prefix: "anything-that-wont-match", then have the agent target this work item for an update β€” the title guard fails and the payload fires.

Same issue applies to tag-prefix with ADO tags containing ##vso[ sequences (since tags are operator-editable semicolon-separated strings).

Suggested fix:

Apply sanitize_text() (which calls neutralize_pipeline_commands()) to current_title and raw_tags before embedding them in error messages:

// In check_prefix_guards, after reading from the ADO response:
use crate::sanitize::sanitize as sanitize_text;

let current_title = ...unwrap_or("");
let safe_title = sanitize_text(current_title);
// use safe_title in the format! string

let raw_tags = ...unwrap_or("");
let safe_tags = sanitize_text(raw_tags);
// use safe_tags in the format! string

Alternatively, apply a lighter-weight sanitize_config() which also neutralizes ##vso[ without altering content semantics. The key is that any ADO-sourced string reaching println! must pass through neutralize_pipeline_commands() first.

Defense-in-depth note: The existing SanitizeContent machinery protects agent-provided fields (title, body, tags supplied by Stage 1). The gap is in data sourced from ADO itself (existing work item values fetched during validation), which has no sanitization before appearing in Stage 3 stdout.


Audit Coverage

Category Status
A: Input Sanitization βœ… Scanned
B: Path Traversal βœ… Scanned
C: Network Bypass βœ… Scanned
D: Credential Exposure βœ… Scanned
E: Logic Flaws βœ… Scanned β€” new finding in PR #368 refactor
F: Supply Chain βœ… Scanned

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

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

Metadata

Metadata

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