From cf0ce7fc73f7384b6e33e95629d9580774e5ef9b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:06:44 +0000 Subject: [PATCH 1/4] Initial plan From 328e14bdb158cbd3a2d1088095c962b98484265f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:12:52 +0000 Subject: [PATCH 2/4] fix: reject template marker delimiters in front matter identity fields Block `{{` in name, description, and trigger fields to prevent second-order template injection where a value like `{{ agent_content }}` would be expanded by a later replacement pass, allowing arbitrary YAML injection into the generated pipeline. Closes #302 Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/91f10480-fd27-4ac7-9105-cbea31200461 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --- src/compile/common.rs | 23 +++++++++++++++++++++-- src/validate.rs | 31 +++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/compile/common.rs b/src/compile/common.rs index 95b55894..2025d663 100644 --- a/src/compile/common.rs +++ b/src/compile/common.rs @@ -137,8 +137,9 @@ pub fn generate_parameters(parameters: &[PipelineParameter]) -> Result { /// Validate front matter `name` and `description` fields. /// /// These values are substituted directly into the pipeline YAML template and must not -/// contain ADO expressions (`${{`, `$(`, `$[`) which could disclose secrets or manipulate -/// pipeline logic. Newlines are also rejected to prevent YAML structure injection. +/// contain ADO expressions (`${{`, `$(`, `$[`), the compiler's own template marker +/// delimiter (`{{`), or newlines — any of which could disclose secrets or manipulate +/// pipeline logic via second-order injection. pub fn validate_front_matter_identity(front_matter: &FrontMatter) -> Result<()> { for (field, value) in [("name", &front_matter.name), ("description", &front_matter.description)] { validate::reject_pipeline_injection(value, field)?; @@ -3146,6 +3147,24 @@ mod tests { assert!(result.unwrap_err().to_string().contains("single line")); } + #[test] + fn test_validate_front_matter_identity_rejects_template_marker_in_name() { + let mut fm = minimal_front_matter(); + fm.name = "{{ agent_content }}".to_string(); + let result = validate_front_matter_identity(&fm); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("template marker")); + } + + #[test] + fn test_validate_front_matter_identity_rejects_template_marker_in_description() { + let mut fm = minimal_front_matter(); + fm.description = "{{ copilot_params }}".to_string(); + let result = validate_front_matter_identity(&fm); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("template marker")); + } + #[test] fn test_validate_front_matter_identity_rejects_newline_in_trigger_pipeline_name() { let mut fm = minimal_front_matter(); diff --git a/src/validate.rs b/src/validate.rs index b7554a0d..6a6dad1b 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -99,6 +99,15 @@ pub fn contains_newline(s: &str) -> bool { s.contains('\n') || s.contains('\r') } +/// Returns true if the string contains the compiler's template marker +/// delimiter (`{{`). Values substituted into the pipeline template must +/// not contain this sequence — otherwise a second-order substitution can +/// inject arbitrary content (e.g., `{{ agent_content }}` in the `name` +/// field would be expanded by a later replacement pass). +pub fn contains_template_marker(s: &str) -> bool { + s.contains("{{") +} + /// Reject ADO template expressions (`${{`), macro expressions (`$(`), and runtime /// expressions (`$[`) in a string value. Parameter definitions should only contain /// literal values — expressions could enable information disclosure or logic manipulation @@ -135,8 +144,8 @@ pub fn reject_ado_expressions_in_value( } /// Reject values that could cause pipeline injection: ADO expressions, -/// pipeline commands (`##vso[`, `##[`), and newlines. A combined check -/// for fields embedded into YAML templates. +/// pipeline commands (`##vso[`, `##[`), template markers (`{{`), and +/// newlines. A combined check for fields embedded into YAML templates. pub fn reject_pipeline_injection(value: &str, field_name: &str) -> Result<()> { if contains_ado_expression(value) { anyhow::bail!( @@ -146,6 +155,13 @@ pub fn reject_pipeline_injection(value: &str, field_name: &str) -> Result<()> { value, ); } + if contains_template_marker(value) { + anyhow::bail!( + "Front matter '{}' contains a template marker delimiter '{{{{{{{{' which is not allowed. \ + Template markers could cause second-order injection into the generated pipeline.", + field_name, + ); + } if contains_newline(value) { anyhow::bail!( "Front matter '{}' must be a single line (no newlines). \ @@ -481,6 +497,15 @@ mod tests { assert!(!contains_newline("single line")); } + #[test] + fn test_contains_template_marker() { + assert!(contains_template_marker("{{ agent_content }}")); + assert!(contains_template_marker("prefix {{ something }} suffix")); + assert!(contains_template_marker("{{no_spaces}}")); + assert!(!contains_template_marker("normal text")); + assert!(!contains_template_marker("just a single { brace")); + } + #[test] fn test_reject_ado_expressions() { assert!(reject_ado_expressions("normal value", "param", "field").is_ok()); @@ -494,6 +519,8 @@ mod tests { assert!(reject_pipeline_injection("normal value", "field").is_ok()); assert!(reject_pipeline_injection("$(SYSTEM_ACCESSTOKEN)", "field").is_err()); assert!(reject_pipeline_injection("value\ninjected", "field").is_err()); + assert!(reject_pipeline_injection("{{ agent_content }}", "field").is_err()); + assert!(reject_pipeline_injection("{{ copilot_params }}", "field").is_err()); } // ── DNS domain validators ─────────────────────────────────────────── From 15240fbf8d5b61c751ed611c938619c0aa7fedf8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:17:50 +0000 Subject: [PATCH 3/4] fix: correct brace escaping in template marker error message Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/91f10480-fd27-4ac7-9105-cbea31200461 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --- src/validate.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validate.rs b/src/validate.rs index 6a6dad1b..5940c535 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -157,7 +157,7 @@ pub fn reject_pipeline_injection(value: &str, field_name: &str) -> Result<()> { } if contains_template_marker(value) { anyhow::bail!( - "Front matter '{}' contains a template marker delimiter '{{{{{{{{' which is not allowed. \ + "Front matter '{}' contains a template marker delimiter '{{{{' which is not allowed. \ Template markers could cause second-order injection into the generated pipeline.", field_name, ); From 5add8a32b512a574e9f3c677aab1f7fe9fe8d510 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:30:27 +0000 Subject: [PATCH 4/4] fix: add pipeline command check and include offending value in error messages Address rust-review feedback: - Add missing contains_pipeline_command() check to reject_pipeline_injection() to match the docstring's claim of ##vso[/##[ protection - Include Found: '{}' in template marker error for debuggability - Add test cases for pipeline command rejection Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/6b4b1bb0-b925-4b27-a00c-d0f4e576b263 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --- src/validate.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/validate.rs b/src/validate.rs index 5940c535..efc8cbec 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -155,11 +155,20 @@ pub fn reject_pipeline_injection(value: &str, field_name: &str) -> Result<()> { value, ); } + if contains_pipeline_command(value) { + anyhow::bail!( + "Front matter '{}' contains an ADO pipeline command ('##vso[' or '##[') which is not allowed. \ + Pipeline commands could manipulate pipeline behavior. Found: '{}'", + field_name, + value, + ); + } if contains_template_marker(value) { anyhow::bail!( "Front matter '{}' contains a template marker delimiter '{{{{' which is not allowed. \ - Template markers could cause second-order injection into the generated pipeline.", + Template markers could cause second-order injection into the generated pipeline. Found: '{}'", field_name, + value, ); } if contains_newline(value) { @@ -521,6 +530,8 @@ mod tests { assert!(reject_pipeline_injection("value\ninjected", "field").is_err()); assert!(reject_pipeline_injection("{{ agent_content }}", "field").is_err()); assert!(reject_pipeline_injection("{{ copilot_params }}", "field").is_err()); + assert!(reject_pipeline_injection("##vso[task.setvariable]x", "field").is_err()); + assert!(reject_pipeline_injection("##[section]foo", "field").is_err()); } // ── DNS domain validators ───────────────────────────────────────────