fix(safe-outputs): make create-work-item body field configurable - #1926
fix(safe-outputs): make create-work-item body field configurable#1926jamesadevine with Copilot wants to merge 29 commits into
Conversation
|
Azure Pipelines: 2 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
|
/review |
|
✅ PR Security Reviewer completed the security review.
|
There was a problem hiding this comment.
Reviewed for ado-aw's compiler-contract invariants (front-matter grammar, safe-output schema/newtype rules, generated-artifact drift, docs sync).
- Front-matter grammar:
description_field: Option<AdoWorkItemFieldRef>with#[serde(default)]is backward compatible — no codemod needed. - Newtype rule: correctly followed —
description-fielduses the newAdoWorkItemFieldRefvalidated newtype (src/secure.rs) backed byvalidate_ado_work_item_field_ref(src/validate.rs), not a rawString. This also protects the/multilineFieldsFormat/{description_field}JSON Patch path from Pointer-syntax corruption. - Docs sync:
docs/front-matter.mdanddocs/safe-outputs.mdwere both updated with the newdescription-fieldoption and defaulting behavior — good. - No generated-artifact drift: this PR only touches
src/anddocs/; noado-scriptbundles, IR/gate schema, or.lock.ymlfiles are implicated, so no drift check applies here.
No merge-blocking compiler-contract issues found. This is a well-scoped, correctly-typed change.
🏗️ Compiler contract review by Compiler Contract Reviewer · auto · 41.4 AIC · ⌖ 2.3 AIC · ⊞ 12.8K
Comment /review to run again
There was a problem hiding this comment.
Solid test coverage overall for the new description-field config and the HTML-preserving sanitize_markdown path (defaults, Bug override, invalid value rejection, executor E2E, sanitizer preserve/neutralize). Two gaps worth addressing before merge:
- No test covers
description-fieldcolliding withcustom-fieldsorSystem.Title, which produces a duplicate JSON Patch path that ADO will likely reject at runtime — this failure mode is currently undetectable except in production. test_sanitize_markdown_neutralizes_active_htmlbundles ~15 distinct attack vectors into one test; a regression in any single vector is masked by the rest passing, making CI failures hard to localize.
Neither is merge-blocking on its own, but the first is a real correctness gap worth at least a regression test.
🧪 Test quality analysis by Test Quality Sentinel · auto · 60.8 AIC · ⌖ 2.84 AIC · ⊞ 11.3K
Comment /review to run again
There was a problem hiding this comment.
Rust code quality review
No merge-blocking Rust defects found in src/mcp.rs, src/safe_outputs/create_work_item.rs, src/sanitize.rs, src/secure.rs, src/validate.rs.
💡 Notes
description_field_forcorrectly derives its fallback to the validatedAdoWorkItemFieldReftype, andvalidate_ado_work_item_field_refrejects.///~characters that would otherwise corrupt the JSON Patch pointer path built infield_op— good defensive design.- Minor, non-blocking:
strip_dangerous_html_tagsandstrip_event_handler_attrs_in_tagsinsrc/sanitize.rseach re-implement an almost identical tag-scanning loop aroundhtml_tag_end; a shared helper that takes a per-tag transform closure would remove the duplication, but it's not a correctness issue and isn't worth a blocking comment. - The
rust-criticsub-agent returned no output for this diff after repeated polling; I completed the review from my own pass only. - Error handling, casts, path handling, and concurrency all looked sound in the changed lines —
anyhow::Result/.context()used consistently, no newunwrap()/expect()on agent-input paths, no lossy casts introduced.
🦀 Rust code quality review by Rust Code Quality Reviewer · auto · 79.3 AIC · ⌖ 2.23 AIC · ⊞ 11.4K
Comment /review to run again
There was a problem hiding this comment.
One merge-blocking finding: the new strip_dangerous_html_tags HTML sanitizer in src/sanitize.rs is single-pass and vulnerable to the classic nested-tag bypass (e.g. <scr<script>ipt>alert(1)</scr</script>ipt> reconstructs a live <script> tag after stripping). I verified this against a standalone extraction of the exact diff algorithm. Since sanitize_markdown is now used for work-item descriptions, and description-field routes Bug items to a Markdown-rendered field, this is a stored-XSS-class regression introduced by this PR (the old sanitize path fully HTML-escaped, so this preserving-HTML replacement needs to close this gap before merge). See inline comment for details and a suggested fix (loop the tag-stripping pass to a fixed point, or use a real tokenizer).
🔒 Security review by PR Security Reviewer · auto · 108.4 AIC · ⌖ 7.28 AIC · ⊞ 11.7K
Comment /review to run again
|
@copilot implement suggestions and resolve blocking findings. |
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
|
/review |
|
✅ Rust Code Quality Reviewer completed the Rust code quality review.
|
There was a problem hiding this comment.
Compiler contract review — no blocking findings
Reviewed the create-work-item description-field change against the ado-aw compiler contracts:
- Identifier newtype:
description-fieldandcustom-fieldskeys are typed asAdoWorkItemFieldRef(validated_string! macro insrc/secure.rs), not rawString— satisfies the secure.rs contract for identifier-like fields used in JSON Patch paths. - Validation coverage:
validate_ado_work_item_field_refruns at Deserialize time (via the macro-generatedDeserializeimpl), so a malformed field ref cannot reachfield_op/JSON Patch construction. - Runtime duplicate-field guard:
validate_patch_fieldscatches case-insensitive collisions betweendescription-field,area-path,tags, andcustom-fieldsbefore the patch is built — good defense given fields are all configurable strings. - Docs:
docs/front-matter.mdanddocs/safe-outputs.mdwere both updated with the newdescription-fieldoption and its per-work-item-type defaulting behavior. - Sanitization:
sanitize_markdownis a deliberate, narrower alternative tosanitize_textfor Markdown bodies (preserves inline HTML, still strips active HTML/handlers/dangerous URL protocols) — used consistently in bothmcp.rs(MCP proposal write) andcreate_work_item.rs(SanitizeContentexecutor path), so the same sanitization applies whether validated at proposal-write time or Stage 3 output-apply time. - Codegen/drift: no changes to
filter_ir.rs, theFactenum, IR, or ado-script bundles in this diff, so no codegen or bundle drift applies here. - Tests: field-ref validation, config defaulting, MCP sanitized-proposal output, and executor patch-payload construction are all covered.
No half-missing contract found. Nothing to request changes on from the compiler-contract perspective.
🏗️ Compiler contract review by Compiler Contract Reviewer · auto · 44.9 AIC · ⌖ 2.71 AIC · ⊞ 12.8K
Comment /review to run again
There was a problem hiding this comment.
Solid test coverage overall: default/custom description-field selection, collision validation across all field kinds, sanitizer preservation vs. stripping of HTML, and an executor-level HTTP round-trip via wiremock. Two gaps worth closing before merge, both advisory (not blocking):
- The description-field collision tests only exercise the
Bug→ReproStepsdefault path; theSystem.Descriptiondefault path (the common case) has no equivalent custom-fields collision test. - The dangerous-HTML fixed-point stripping loop is only proven against
<script>nesting; the other 9 tag names it guards (iframe,svg,style, etc.) aren't tested against the same folding attack.
No weakened/removed assertions detected, and the new validate_ado_work_item_field_ref and JSON-patch collision paths look well covered on the error side.
🧪 Test quality analysis by Test Quality Sentinel · auto · 54.4 AIC · ⌖ 1.84 AIC · ⊞ 11.3K
Comment /review to run again
There was a problem hiding this comment.
This diff introduces sanitize_markdown to preserve inline HTML in work-item descriptions instead of fully HTML-escaping them, and adds a matching event-handler/dangerous-tag stripper. I found a stored-XSS bypass in that stripper (inline comment on src/sanitize.rs:264): the event-handler regexes require a literal space or < before onXXX=, so <img/onerror=...> (slash separator) and similarly-crafted on* attributes on non-blocklisted tags (img, a, body, etc.) pass through untouched. Everything else in the diff (field-ref validation, token/config handling, dangerous-tag nested-stripping loop) looked sound.
🔒 Security review by PR Security Reviewer · auto · 87.4 AIC · ⌖ 2.03 AIC · ⊞ 11.7K
Comment /review to run again
There was a problem hiding this comment.
Rust-quality pass on the changed files (src/mcp.rs, src/safe_outputs/create_work_item.rs, src/sanitize.rs, src/secure.rs, src/validate.rs). Solid error handling throughout (validated newtypes, anyhow::ensure!, no risky unwrap()/expect() on untrusted paths — the one expect() in the tag-stripping loops is provably safe since it follows an index < input.len() check). Left three advisory comments: a possible quadratic-time worst case in the new fixed-point HTML tag stripper running before the content-size cap, a comment left orphaned by code relocation, and a HashMap-iteration-order inconsistency between custom-field validation (sorted) and patch construction (unsorted). None of these are correctness-breaking for the primary use case, so COMMENT rather than blocking.
The rust-critic sub-agent did not return output in time; findings above are from my own pass only.
Themes reviewed
- Error handling /
anyhowcontext usage — clean. - New
AdoWorkItemFieldRefvalidated type andvalidate_ado_work_item_field_ref— well-tested, correctly gates dot-segment identifiers. HashMap<AdoWorkItemFieldRef, String>key-type migration — behaviorally sound, but ordering divergence noted above.- New
sanitize_markdownHTML-stripping pipeline — algorithmically more expensive than the existingescape_html_tags; flagged the ordering-vs-size-limit concern.
🦀 Rust code quality review by Rust Code Quality Reviewer · auto · 129.5 AIC · ⌖ 1.91 AIC · ⊞ 11.4K
Comment /review to run again
|
@copilot address final review comments |
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Addressed in 48b6a6f through a287f33. The follow-up strips slash-separated event-handler attributes, pre-caps Markdown before the expensive HTML pass, makes custom-field patch ordering deterministic, and adds the requested collision/folded-tag sanitizer coverage. |
|
/review |
|
✅ PR Security Reviewer completed the security review. Caution agentic threat detected DetailsPotential security threats were detected in the agent output. Review the workflow run logs for details.
|
There was a problem hiding this comment.
Solid test coverage overall: the new description-field config, patch-field collision validation, and the HTML-preserving markdown sanitizer all have targeted unit tests plus one mocked-HTTP executor test. Found a few gaps that would let regressions ship silently — none are blocking, all are advisory additions to widen coverage on the case-insensitive Bug-type match and the full collision matrix.
🧪 Test quality analysis by Test Quality Sentinel · auto · 65.7 AIC · ⌖ 2.65 AIC · ⊞ 11.3K
Comment /review to run again
There was a problem hiding this comment.
Rust review — no blocking issues found
Reviewed src/mcp.rs, src/safe_outputs/create_work_item.rs, src/sanitize.rs, src/secure.rs, src/validate.rs.
AdoWorkItemFieldRefvalidation is strict and correctly guards against JSON Pointer path corruption in/fields/<ref>.sorted_custom_fieldsfixes what would otherwise be nondeterministicHashMapiteration in generated JSON Patch output — good catch, and it's covered bytest_sorted_custom_fields_orders_case_insensitively.validate_patch_fieldscorrectly detects field collisions (title/description/custom-fields) before use, avoiding silent data loss on the ADO patch document.- The new
sanitize_markdown/dangerous-tag stripping logic insanitize.rs(fixed-point loop overstrip_dangerous_html_tags, event-handler-attribute stripping, protected fenced-code ranges) is thorough and has strong adversarial test coverage (nested/folded tag reconstruction, attribute-span folding, unclosed tags). - No
unwrap()/expect()on user-reachable paths; error handling usesanyhow::ensure!/bail!with actionable messages throughout. cargo buildsucceeds cleanly.
The Compiler Contract reviewer owns the front-matter/schema/docs side of this change; nothing further from a pure Rust-engineering standpoint.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
spsprodeus21.vssps.visualstudio.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "spsprodeus21.vssps.visualstudio.com"See Network Configuration for more information.
🦀 Rust code quality review by Rust Code Quality Reviewer · auto · 66.5 AIC · ⌖ 5.98 AIC · ⊞ 11.4K
Comment /review to run again
There was a problem hiding this comment.
Compiler-contract review: front-matter grammar, safe-output docs, and the new AdoWorkItemFieldRef newtype all look correctly wired (properly uses validated_string!/src/validate.rs, not a raw String). Tests are thorough and pass locally (cargo test --bin ado-aw create_work_item). One finding posted inline: the new validate_patch_fields collision check only runs in the Stage 3 executor, not in the compile-time validate_work_item_assignment_outputs_config, so a misconfigured description-field/custom-fields collision is discovered only after a pipeline runs, not at ado-aw compile/lint time.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
spsprodeus21.vssps.visualstudio.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "spsprodeus21.vssps.visualstudio.com"See Network Configuration for more information.
🏗️ Compiler contract review by Compiler Contract Reviewer · auto · 107.6 AIC · ⌖ 2.45 AIC · ⊞ 12.8K
Comment /review to run again
There was a problem hiding this comment.
Caution
agentic threat detected
Threat detection flagged this output in warn mode. Manual review is REQUIRED before any follow-up automation.
Details
Potential security threats were detected in the agent output.
Review the workflow run logs for details.
Reviewed the sanitize/validate/create-work-item changes. The new AdoWorkItemFieldRef typed newtype, validate_ado_work_item_field_ref, and the config sanitization (SanitizeConfig impl covering all string fields including custom-field keys/values and artifact-link) look correctly scoped and don't weaken any existing control.
One finding worth blocking on: sanitize_markdown (new src/sanitize.rs function used by create-work-item descriptions) preserves raw HTML while still relying on the pre-existing sanitize_url_protocols, which does a literal substring search for (redacted) That check is bypassable with an embedded whitespace/control character in the scheme (java\tscript:), and because the surrounding HTML is no longer escaped to inert entities (unlike the original sanitize()path), this bypass is now live rather than latent — a malicious agent-authored work item description could smuggle a(redacted) URI into a rendered <a href>/<img src>. See inline comment for a reproduction and suggested fix (normalize/strip whitespace from the scheme before matching, or operate on parsed attribute values).
Everything else — token handling, custom-field key typing, patch-field duplicate-field validation, and the dangerous-tag/event-handler stripping logic — is sound.
🔒 Security review by PR Security Reviewer · auto · 87.8 AIC · ⌖ 11 AIC · ⊞ 11.7K
Comment /review to run again
Superseded by updated review from same workflow.
Summary
create-work-itemalways wrote agent-generated body content toSystem.Description, which makes Bug work items appear blank in processes whose Bug form rendersMicrosoft.VSTS.TCM.ReproStepsinstead. It also escaped body HTML before storing the field as Markdown, producing unreadable content.safe-outputs.create-work-item.description-field.BugtoMicrosoft.VSTS.TCM.ReproSteps.System.Description.Body sanitization
Docs and coverage
description-fieldand Markdown body semantics.Test plan
cargo test create_work_itemcargo test sanitize_markdowncargo test validate_ado_work_item_field_refcreate-work-itemhardcodesSystem.Description, so Bug work items are filed blank #1924