@@ -135,17 +135,24 @@ impl FilesystemIrm {
135135
136136 /// Extract path from host call arguments
137137 fn extract_path ( & self , call : & HostCall ) -> Option < String > {
138+ let allow_bare_string_paths = call. function . contains ( "path" ) ;
139+
138140 for arg in & call. args {
139141 if let Some ( s) = arg. as_str ( ) {
140- if self . looks_like_path ( s) {
142+ if self . looks_like_path ( s)
143+ || ( allow_bare_string_paths && self . looks_like_bare_filename ( s) )
144+ {
141145 return Some ( s. to_string ( ) ) ;
142146 }
143147 }
144148
145149 if let Some ( obj) = arg. as_object ( ) {
146150 for key in [ "path" , "file_path" , "target_path" ] {
147151 if let Some ( path) = obj. get ( key) . and_then ( |value| value. as_str ( ) ) {
148- if self . looks_like_path ( path) || self . has_parent_traversal ( path) {
152+ if self . looks_like_path ( path)
153+ || self . has_parent_traversal ( path)
154+ || self . looks_like_bare_filename ( path)
155+ {
149156 return Some ( path. to_string ( ) ) ;
150157 }
151158 }
@@ -176,6 +183,31 @@ impl FilesystemIrm {
176183 value. contains ( '/' ) && !value. contains ( "://" )
177184 }
178185
186+ fn looks_like_bare_filename ( & self , value : & str ) -> bool {
187+ let value = value. trim ( ) ;
188+ if value. is_empty ( ) {
189+ return false ;
190+ }
191+
192+ if value. contains ( "://" ) {
193+ return false ;
194+ }
195+
196+ if value == "." || value == ".." {
197+ return false ;
198+ }
199+
200+ if value. contains ( '/' ) || value. contains ( '\\' ) {
201+ return false ;
202+ }
203+
204+ if value. bytes ( ) . all ( |b| b. is_ascii_digit ( ) ) {
205+ return false ;
206+ }
207+
208+ !value. chars ( ) . any ( |ch| ch. is_control ( ) )
209+ }
210+
179211 fn has_parent_traversal ( & self , path : & str ) -> bool {
180212 path. replace ( '\\' , "/" ) . split ( '/' ) . any ( |seg| seg == ".." )
181213 }
@@ -346,10 +378,32 @@ mod tests {
346378 Some ( "../../etc/passwd" . to_string( ) )
347379 ) ;
348380
381+ let call = HostCall :: new ( "path_open" , vec ! [ serde_json:: json!( "README.md" ) ] ) ;
382+ assert_eq ! ( irm. extract_path( & call) , Some ( "README.md" . to_string( ) ) ) ;
383+
384+ let call = HostCall :: new (
385+ "fd_write" ,
386+ vec ! [ serde_json:: json!( { "target_path" : "config.json" } ) ] ,
387+ ) ;
388+ assert_eq ! ( irm. extract_path( & call) , Some ( "config.json" . to_string( ) ) ) ;
389+
349390 let call = HostCall :: new ( "fd_read" , vec ! [ serde_json:: json!( 123 ) ] ) ;
350391 assert_eq ! ( irm. extract_path( & call) , None ) ;
351392 }
352393
394+ #[ tokio:: test]
395+ async fn filesystem_irm_allows_bare_filename_for_path_style_calls ( ) {
396+ let irm = FilesystemIrm :: new ( ) ;
397+ let policy = Policy :: default ( ) ;
398+ let call = HostCall :: new ( "path_open" , vec ! [ serde_json:: json!( "README.md" ) ] ) ;
399+ let decision = irm. evaluate ( & call, & policy) . await ;
400+
401+ assert ! (
402+ decision. is_allowed( ) ,
403+ "bare filename should be treated as a valid filesystem path in path-style calls"
404+ ) ;
405+ }
406+
353407 #[ tokio:: test]
354408 async fn filesystem_irm_denies_parent_traversal_relative_paths ( ) {
355409 let irm = FilesystemIrm :: new ( ) ;
0 commit comments