Skip to content

πŸ”΄ Red Team Audit β€” High: link-work-items area-path target scope silently fail-opensΒ #2261

Description

@github-actions

πŸ”΄ Red Team Security Audit

Audit focus: Category E β€” Logic & Authorization Flaws (link-work-items safe output)
Severity: High

Findings

# Vulnerability Severity File(s) Exploitable?
1 Area-path target: scope silently fail-opens in link-work-items High src/safe_outputs/link_work_items.rs:208-230, src/safe_outputs/comment_on_work_item.rs:76-83 Yes

Details

Finding 1: link-work-items area-path target scoping is not enforced (fail-open)

Description: The link-work-items safe output reuses comment_on_work_item::CommentTarget for its target: configuration field, and its doc comment explicitly states it "Accepts the same values as comment-on-work-item: "*", a single ID, a list of IDs, or an area path string." However, CommentTarget::allows_id() returns None (not Some(false)) for area-path string targets β€” by design, since resolving an area path requires an API call (get_work_item_area_path), which only comment_on_work_item.rs implements and calls.

In comment_on_work_item.rs, the caller (validate_target_policy) correctly branches on None and performs the deferred API-based area-path check (get_work_item_area_path + prefix match) before allowing the operation.

In link_work_items.rs::execute_impl, the equivalent gating logic is:

if source_needs_target && target.allows_id(source_id) == Some(false) {
    return Ok(ExecutionResult::failure(...));
}
if target_needs_target && target.allows_id(target_id) == Some(false) {
    return Ok(ExecutionResult::failure(...));
}

This only rejects when allows_id() returns Some(false). When an operator configures an area-path string (e.g. target: "MyProject\\Area\\Security"), allows_id() always returns None for any work-item ID β€” so the == Some(false) check never matches, and every source_id/target_id is silently permitted regardless of its actual area path. The get_work_item_area_path helper needed to perform the real check is a private fn in comment_on_work_item.rs and is never invoked from link_work_items.rs, so there is no code path that could ever reject a request under an area-path-scoped configuration β€” this is a structural fail-open, not merely a bug that trips occasionally.

Attack vector: An operator configures safe-outputs.link-work-items.target with an area-path string believing it restricts which work items the agent can link (matching the documented behavior shared with comment-on-work-item). A compromised/malicious agent (Stage 1, driven by prompt injection from untrusted repo content) can then call the link-work-items MCP tool with source_id/target_id pointing to any work item in the project β€” including security-sensitive or out-of-scope work items β€” and the Stage 3 executor will link them without any area-path validation.

Proof of concept:

safe-outputs:
  link-work-items:
    target: "MyProject\\Restricted\\SecurityTeam"
    allowed-link-types: [related]

An agent (or injected instruction) calls the link-work-items tool with:

{"source_id": 99999, "target_id": 1, "link_type": "related"}

where work item 99999 is outside the MyProject\Restricted\SecurityTeam area path. Because CommentTarget::allows_id(99999) returns None (not Some(false)) for this string target, link_work_items.rs treats it as allowed and links the items via the ADO REST API β€” bypassing the operator's intended area-path restriction entirely.

Impact: Silent authorization-policy bypass. Any workflow author who scopes link-work-items.target to an area path (rather than "*" or a numeric ID/list) gets no actual enforcement β€” the tool behaves as if target: "*" were configured, contrary to documented and intended behavior. This could let a compromised agent link arbitrary/unrelated work items (e.g., attaching spam/malicious relations to security or compliance work items, or fabricating false "related"/"duplicate" relationships to manipulate triage), while operators believe the area-path scope is protecting those items.

Suggested fix: Either (a) make the area-path resolution helper in comment_on_work_item.rs pub(crate) and call it from link_work_items.rs for both source_id and target_id when allows_id() returns None (mirroring validate_target_policy's behavior), or (b) until that is implemented, fail closed in link_work_items.rs by rejecting (not permitting) when allows_id() returns None, and updating the doc comment to state that area-path targets are not yet supported for link-work-items.


Audit Coverage

Category Status
A: Input Sanitization βœ… Scanned (no new finding; re-confirmed prior mitigations, Rounds 1-22)
B: Path Traversal βœ… Scanned (no new finding; re-confirmed prior mitigations)
C: Network Bypass βœ… Scanned (no new finding; re-confirmed prior mitigations)
D: Credential Exposure βœ… Scanned (no new finding; re-confirmed prior mitigations)
E: Logic Flaws βœ… Scanned β€” new finding above (link-work-items area-path fail-open)
F: Supply Chain βœ… Scanned (no new finding; MCPG tag-only pinning previously reported in #209)

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

Generated by Red Team Security Auditor Β· auto Β· 91.3 AIC Β· βŒ– 2.31 AIC Β· ⊞ 11.6K Β· β—·

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingrustPull requests that update rust codesecurity

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions