@@ -50,6 +50,10 @@ impl Validate for UploadWorkitemAttachmentParams {
5050 !contains_newline( & self . file_path) ,
5151 "file_path must not contain newlines or carriage returns"
5252 ) ;
53+ ensure ! (
54+ !self . file_path. contains( "##vso[" ) && !self . file_path. contains( "##[" ) ,
55+ "file_path must not contain Azure DevOps pipeline command sequences"
56+ ) ;
5357 ensure ! (
5458 !self
5559 . file_path
@@ -222,10 +226,10 @@ impl Executor for UploadWorkitemAttachmentResult {
222226 // acceptable because the injection risk from binary attachments is negligible.
223227 if let Ok ( text) = std:: str:: from_utf8 ( & file_bytes) {
224228 if text. contains ( "##vso[" ) {
225- return Ok ( ExecutionResult :: failure ( format ! (
226- "File '{}' contains '##vso[' command injection sequence" ,
227- self . file_path
228- ) ) ) ;
229+ return Ok ( ExecutionResult :: failure (
230+ "Uploaded file contains '##vso[' command injection sequence — upload rejected"
231+ . to_string ( ) ,
232+ ) ) ;
229233 }
230234 }
231235
@@ -521,6 +525,24 @@ mod tests {
521525 assert ! ( result. is_err( ) ) ;
522526 }
523527
528+ #[ test]
529+ fn test_validation_rejects_pipeline_command_sequences_in_file_path ( ) {
530+ let vso = UploadWorkitemAttachmentParams {
531+ work_item_id : 42 ,
532+ file_path : "##vso[task.setvariable variable=EXPLOIT]value.txt" . to_string ( ) ,
533+ comment : None ,
534+ } ;
535+ let shorthand = UploadWorkitemAttachmentParams {
536+ work_item_id : 42 ,
537+ file_path : "##[error]value.txt" . to_string ( ) ,
538+ comment : None ,
539+ } ;
540+ let vso_result: Result < UploadWorkitemAttachmentResult , _ > = vso. try_into ( ) ;
541+ let shorthand_result: Result < UploadWorkitemAttachmentResult , _ > = shorthand. try_into ( ) ;
542+ assert ! ( vso_result. is_err( ) ) ;
543+ assert ! ( shorthand_result. is_err( ) ) ;
544+ }
545+
524546 #[ test]
525547 fn test_result_serializes_correctly ( ) {
526548 let params = UploadWorkitemAttachmentParams {
0 commit comments