@@ -9,6 +9,14 @@ use crate::models::{Package, PackageDetail, Source};
99
1010pub struct CliBackend ;
1111
12+ /// Strip ASCII control characters (0x00–0x1F, 0x7F) except tab and newline.
13+ /// Prevents ANSI escape injection from malicious package metadata.
14+ fn sanitize_text ( s : & str ) -> String {
15+ s. chars ( )
16+ . filter ( |& c| c == '\t' || c == '\n' || ( c >= ' ' && c != '\x7F' ) )
17+ . collect ( )
18+ }
19+
1220impl CliBackend {
1321 pub fn new ( ) -> Self {
1422 Self
@@ -240,11 +248,11 @@ impl CliBackend {
240248 }
241249
242250 Some ( Package {
243- name : name_idx. map ( & field) . unwrap_or_default ( ) ,
244- id,
245- version : ver_idx. map ( & field) . unwrap_or_default ( ) ,
246- source : source_idx. map ( & field) . unwrap_or_default ( ) ,
247- available_version : avail_idx. map ( & field) . unwrap_or_default ( ) ,
251+ name : sanitize_text ( & name_idx. map ( & field) . unwrap_or_default ( ) ) ,
252+ id : sanitize_text ( & id ) ,
253+ version : sanitize_text ( & ver_idx. map ( & field) . unwrap_or_default ( ) ) ,
254+ source : sanitize_text ( & source_idx. map ( & field) . unwrap_or_default ( ) ) ,
255+ available_version : sanitize_text ( & avail_idx. map ( & field) . unwrap_or_default ( ) ) ,
248256 } )
249257 }
250258
@@ -266,11 +274,13 @@ impl CliBackend {
266274 if bracket_end > bracket_start && !trimmed. contains ( ':' ) {
267275 let before_bracket = trimmed[ ..bracket_start] . trim ( ) ;
268276 // Skip the prefix word ("Found", "Gefunden", etc.)
269- detail. name = before_bracket
270- . split_once ( ' ' )
271- . map ( |( _, name) | name. trim ( ) . to_string ( ) )
272- . unwrap_or_default ( ) ;
273- detail. id = trimmed[ bracket_start + 1 ..bracket_end] . to_string ( ) ;
277+ detail. name = sanitize_text (
278+ & before_bracket
279+ . split_once ( ' ' )
280+ . map ( |( _, name) | name. trim ( ) . to_string ( ) )
281+ . unwrap_or_default ( ) ,
282+ ) ;
283+ detail. id = sanitize_text ( & trimmed[ bracket_start + 1 ..bracket_end] . to_string ( ) ) ;
274284 i += 1 ;
275285 continue ;
276286 }
@@ -282,8 +292,8 @@ impl CliBackend {
282292 let key = key. trim ( ) ;
283293 let value = value. trim ( ) . to_string ( ) ;
284294 match Self :: normalize_show_key ( key) {
285- "version" => detail. version = value,
286- "publisher" => detail. publisher = value,
295+ "version" => detail. version = sanitize_text ( & value) ,
296+ "publisher" => detail. publisher = sanitize_text ( & value) ,
287297 "description" => {
288298 // Description value may be on this line or on indented continuation lines
289299 let mut desc = value;
@@ -294,16 +304,16 @@ impl CliBackend {
294304 }
295305 desc. push_str ( lines[ i] . trim ( ) ) ;
296306 }
297- detail. description = desc;
307+ detail. description = sanitize_text ( & desc) ;
298308 }
299- "homepage" => detail. homepage = value,
309+ "homepage" => detail. homepage = sanitize_text ( & value) ,
300310 "publisher_url" => {
301311 if detail. homepage . is_empty ( ) {
302- detail. homepage = value;
312+ detail. homepage = sanitize_text ( & value) ;
303313 }
304314 }
305- "license" => detail. license = value,
306- "source" => detail. source = value,
315+ "license" => detail. license = sanitize_text ( & value) ,
316+ "source" => detail. source = sanitize_text ( & value) ,
307317 _ => { }
308318 }
309319 }
@@ -708,4 +718,31 @@ Microsoft Visual Studio Code Microsoft.VisualStudioCode 1.95.3 1.96.0
708718 assert_eq ! ( packages[ 0 ] . id, "Google.Chrome" ) ;
709719 assert_eq ! ( packages[ 1 ] . id, "Microsoft.VisualStudioCode" ) ;
710720 }
721+
722+ #[ test]
723+ fn sanitize_strips_ansi_escape_from_package_name ( ) {
724+ // Direct test of sanitize_text helper
725+ let dirty = "Evil\x1b ]52;c;payload\x07 App" ;
726+ let clean = super :: sanitize_text ( dirty) ;
727+ assert ! ( !clean. contains( '\x1b' ) , "ESC must be stripped" ) ;
728+ assert ! ( !clean. contains( '\x07' ) , "BEL must be stripped" ) ;
729+ assert_eq ! ( clean, "Evil]52;c;payloadApp" ) ;
730+
731+ // Verify tab and newline are preserved
732+ assert_eq ! ( super :: sanitize_text( "a\t b\n c" ) , "a\t b\n c" ) ;
733+
734+ // End-to-end: package table with embedded escape in name
735+ let backend = CliBackend :: new ( ) ;
736+ let output = "\
737+ Name Id Version Source
738+ ----------------------------------------------------------------------------------
739+ Google\x1b [2JChrome Google.Chrome 131.0 winget
740+ " ;
741+ let packages = backend. parse_packages_from_table ( output) ;
742+ assert_eq ! ( packages. len( ) , 1 ) ;
743+ assert ! (
744+ !packages[ 0 ] . name. contains( '\x1b' ) ,
745+ "ANSI escape must be stripped from parsed package name"
746+ ) ;
747+ }
711748}
0 commit comments