@@ -99,6 +99,15 @@ pub fn contains_newline(s: &str) -> bool {
9999 s. contains ( '\n' ) || s. contains ( '\r' )
100100}
101101
102+ /// Returns true if the string contains the compiler's template marker
103+ /// delimiter (`{{`). Values substituted into the pipeline template must
104+ /// not contain this sequence — otherwise a second-order substitution can
105+ /// inject arbitrary content (e.g., `{{ agent_content }}` in the `name`
106+ /// field would be expanded by a later replacement pass).
107+ pub fn contains_template_marker ( s : & str ) -> bool {
108+ s. contains ( "{{" )
109+ }
110+
102111/// Reject ADO template expressions (`${{`), macro expressions (`$(`), and runtime
103112/// expressions (`$[`) in a string value. Parameter definitions should only contain
104113/// literal values — expressions could enable information disclosure or logic manipulation
@@ -135,8 +144,8 @@ pub fn reject_ado_expressions_in_value(
135144}
136145
137146/// Reject values that could cause pipeline injection: ADO expressions,
138- /// pipeline commands (`##vso[`, `##[`), and newlines. A combined check
139- /// for fields embedded into YAML templates.
147+ /// pipeline commands (`##vso[`, `##[`), template markers (`{{`), and
148+ /// newlines. A combined check for fields embedded into YAML templates.
140149pub fn reject_pipeline_injection ( value : & str , field_name : & str ) -> Result < ( ) > {
141150 if contains_ado_expression ( value) {
142151 anyhow:: bail!(
@@ -146,6 +155,13 @@ pub fn reject_pipeline_injection(value: &str, field_name: &str) -> Result<()> {
146155 value,
147156 ) ;
148157 }
158+ if contains_template_marker ( value) {
159+ anyhow:: bail!(
160+ "Front matter '{}' contains a template marker delimiter '{{{{{{{{' which is not allowed. \
161+ Template markers could cause second-order injection into the generated pipeline.",
162+ field_name,
163+ ) ;
164+ }
149165 if contains_newline ( value) {
150166 anyhow:: bail!(
151167 "Front matter '{}' must be a single line (no newlines). \
@@ -481,6 +497,15 @@ mod tests {
481497 assert ! ( !contains_newline( "single line" ) ) ;
482498 }
483499
500+ #[ test]
501+ fn test_contains_template_marker ( ) {
502+ assert ! ( contains_template_marker( "{{ agent_content }}" ) ) ;
503+ assert ! ( contains_template_marker( "prefix {{ something }} suffix" ) ) ;
504+ assert ! ( contains_template_marker( "{{no_spaces}}" ) ) ;
505+ assert ! ( !contains_template_marker( "normal text" ) ) ;
506+ assert ! ( !contains_template_marker( "just a single { brace" ) ) ;
507+ }
508+
484509 #[ test]
485510 fn test_reject_ado_expressions ( ) {
486511 assert ! ( reject_ado_expressions( "normal value" , "param" , "field" ) . is_ok( ) ) ;
@@ -494,6 +519,8 @@ mod tests {
494519 assert ! ( reject_pipeline_injection( "normal value" , "field" ) . is_ok( ) ) ;
495520 assert ! ( reject_pipeline_injection( "$(SYSTEM_ACCESSTOKEN)" , "field" ) . is_err( ) ) ;
496521 assert ! ( reject_pipeline_injection( "value\n injected" , "field" ) . is_err( ) ) ;
522+ assert ! ( reject_pipeline_injection( "{{ agent_content }}" , "field" ) . is_err( ) ) ;
523+ assert ! ( reject_pipeline_injection( "{{ copilot_params }}" , "field" ) . is_err( ) ) ;
497524 }
498525
499526 // ── DNS domain validators ───────────────────────────────────────────
0 commit comments