1- use std:: { fs , io , path :: PathBuf } ;
1+ use std:: io ;
22
33use crate :: config:: { data, locator} ;
44
@@ -10,8 +10,10 @@ pub enum Error {
1010 Data ( #[ from] data:: Error ) ,
1111 #[ error( "failed to find cache entry {0}" ) ]
1212 NotFound ( String ) ,
13+ #[ error( "invalid cache entry ID \" {0}\" : expected a ULID" ) ]
14+ InvalidId ( String ) ,
1315 #[ error( transparent) ]
14- SerdeJson ( #[ from] serde_json :: Error ) ,
16+ Io ( #[ from] std :: io :: Error ) ,
1517}
1618
1719#[ derive( Debug , clap:: Parser , Clone ) ]
@@ -24,15 +26,72 @@ pub struct Cmd {
2426
2527impl Cmd {
2628 pub fn run ( & self ) -> Result < ( ) , Error > {
27- let file = self . file ( ) ?;
29+ let id: ulid:: Ulid = self
30+ . id
31+ . parse ( )
32+ . map_err ( |_| Error :: InvalidId ( self . id . clone ( ) ) ) ?;
33+ let file = data:: actions_dir ( ) ?
34+ . join ( id. to_string ( ) )
35+ . with_extension ( "json" ) ;
2836 tracing:: debug!( "reading file {}" , file. display( ) ) ;
29- let mut file = fs:: File :: open ( file) . map_err ( |_| Error :: NotFound ( self . id . clone ( ) ) ) ?;
30- let mut stdout = io:: stdout ( ) ;
31- let _ = io:: copy ( & mut file, & mut stdout) ;
37+ let mut f = std:: fs:: File :: open ( & file) . map_err ( |e| {
38+ if e. kind ( ) == io:: ErrorKind :: NotFound {
39+ Error :: NotFound ( self . id . clone ( ) )
40+ } else {
41+ Error :: Io ( e)
42+ }
43+ } ) ?;
44+ io:: copy ( & mut f, & mut io:: stdout ( ) ) ?;
3245 Ok ( ( ) )
3346 }
47+ }
48+
49+ #[ cfg( test) ]
50+ mod tests {
51+ use super :: * ;
52+ use crate :: test_utils:: EnvGuard ;
53+ use serial_test:: serial;
54+
55+ #[ test]
56+ #[ serial]
57+ fn path_traversal_via_dotdot_is_rejected ( ) {
58+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
59+ let _guard = EnvGuard :: set ( "STELLAR_DATA_HOME" , tmp. path ( ) ) ;
60+
61+ let outside = tmp. path ( ) . join ( "outside.json" ) ;
62+ std:: fs:: write ( & outside, r#"{"leaked":true}"# ) . unwrap ( ) ;
63+
64+ let cmd = Cmd {
65+ id : "../outside" . to_string ( ) ,
66+ } ;
67+
68+ let err = cmd. run ( ) . expect_err ( "expected error for path-traversal ID" ) ;
69+ assert ! (
70+ matches!( err, Error :: InvalidId ( _) ) ,
71+ "expected InvalidId, got {err:?}"
72+ ) ;
73+ }
74+
75+ #[ test]
76+ #[ serial]
77+ fn absolute_path_id_is_rejected ( ) {
78+ let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
79+ let _guard = EnvGuard :: set ( "STELLAR_DATA_HOME" , tmp. path ( ) ) ;
80+
81+ let outside = tmp. path ( ) . join ( "outside.json" ) ;
82+ std:: fs:: write ( & outside, r#"{"leaked":true}"# ) . unwrap ( ) ;
83+
84+ let abs_id = outside
85+ . to_str ( )
86+ . unwrap ( )
87+ . trim_end_matches ( ".json" )
88+ . to_string ( ) ;
89+ let cmd = Cmd { id : abs_id } ;
3490
35- pub fn file ( & self ) -> Result < PathBuf , Error > {
36- Ok ( data:: actions_dir ( ) ?. join ( & self . id ) . with_extension ( "json" ) )
91+ let err = cmd. run ( ) . expect_err ( "expected error for absolute-path ID" ) ;
92+ assert ! (
93+ matches!( err, Error :: InvalidId ( _) ) ,
94+ "expected InvalidId, got {err:?}"
95+ ) ;
3796 }
3897}
0 commit comments