π΄ 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 Β· β·
π΄ Red Team Security Audit
Audit focus: Category E β Logic & Authorization Flaws (
link-work-itemssafe output)Severity: High
Findings
target:scope silently fail-opens inlink-work-itemssrc/safe_outputs/link_work_items.rs:208-230,src/safe_outputs/comment_on_work_item.rs:76-83Details
Finding 1:
link-work-itemsarea-path target scoping is not enforced (fail-open)Description: The
link-work-itemssafe output reusescomment_on_work_item::CommentTargetfor itstarget: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()returnsNone(notSome(false)) for area-path string targets β by design, since resolving an area path requires an API call (get_work_item_area_path), which onlycomment_on_work_item.rsimplements and calls.In
comment_on_work_item.rs, the caller (validate_target_policy) correctly branches onNoneand 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:This only rejects when
allows_id()returnsSome(false). When an operator configures an area-path string (e.g.target: "MyProject\\Area\\Security"),allows_id()always returnsNonefor any work-item ID β so the== Some(false)check never matches, and everysource_id/target_idis silently permitted regardless of its actual area path. Theget_work_item_area_pathhelper needed to perform the real check is a privatefnincomment_on_work_item.rsand is never invoked fromlink_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.targetwith an area-path string believing it restricts which work items the agent can link (matching the documented behavior shared withcomment-on-work-item). A compromised/malicious agent (Stage 1, driven by prompt injection from untrusted repo content) can then call thelink-work-itemsMCP tool withsource_id/target_idpointing 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:
An agent (or injected instruction) calls the
link-work-itemstool with:{"source_id": 99999, "target_id": 1, "link_type": "related"}where work item
99999is outside theMyProject\Restricted\SecurityTeamarea path. BecauseCommentTarget::allows_id(99999)returnsNone(notSome(false)) for this string target,link_work_items.rstreats 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.targetto an area path (rather than"*"or a numeric ID/list) gets no actual enforcement β the tool behaves as iftarget: "*"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.rspub(crate)and call it fromlink_work_items.rsfor bothsource_idandtarget_idwhenallows_id()returnsNone(mirroringvalidate_target_policy's behavior), or (b) until that is implemented, fail closed inlink_work_items.rsby rejecting (not permitting) whenallows_id()returnsNone, and updating the doc comment to state that area-path targets are not yet supported forlink-work-items.Audit Coverage
link-work-itemsarea-path fail-open)This issue was created by the automated red team security auditor.