@@ -6066,11 +6066,17 @@ impl Interpreter {
60666066 . variables
60676067 . keys ( )
60686068 . filter ( |k| k. starts_with ( prefix. as_str ( ) ) )
6069+ // THREAT[TM-INJ-009]: Hide internal marker variables
6070+ . filter ( |k| !Self :: is_internal_variable ( k) )
60696071 . cloned ( )
60706072 . collect ( ) ;
60716073 // Also check env
60726074 for k in self . env . keys ( ) {
6073- if k. starts_with ( prefix. as_str ( ) ) && !names. contains ( k) {
6075+ if k. starts_with ( prefix. as_str ( ) )
6076+ && !names. contains ( k)
6077+ // THREAT[TM-INJ-009]: Hide internal marker variables
6078+ && !Self :: is_internal_variable ( k)
6079+ {
60746080 names. push ( k. clone ( ) ) ;
60756081 }
60766082 }
@@ -7548,10 +7554,24 @@ impl Interpreter {
75487554 s. to_string ( )
75497555 }
75507556
7557+ /// THREAT[TM-INJ-009]: Check if a variable name is an internal marker.
7558+ fn is_internal_variable ( name : & str ) -> bool {
7559+ name. starts_with ( "_NAMEREF_" )
7560+ || name. starts_with ( "_READONLY_" )
7561+ || name. starts_with ( "_UPPER_" )
7562+ || name. starts_with ( "_LOWER_" )
7563+ || name == "_SHIFT_COUNT"
7564+ || name == "_SET_POSITIONAL"
7565+ }
7566+
75517567 /// Set a variable, respecting dynamic scoping.
75527568 /// If the variable is declared `local` in any active call frame, update that frame.
75537569 /// Otherwise, set in global variables.
75547570 fn set_variable ( & mut self , name : String , value : String ) {
7571+ // THREAT[TM-INJ-009]: Block user assignment to internal marker variables
7572+ if Self :: is_internal_variable ( & name) {
7573+ return ;
7574+ }
75557575 // Resolve nameref: if `name` is a nameref, assign to the target instead
75567576 let resolved = self . resolve_nameref ( & name) . to_string ( ) ;
75577577 // Apply case conversion attributes (declare -l / declare -u)
@@ -9436,4 +9456,31 @@ mod tests {
94369456 assert_eq ! ( result. exit_code, 0 ) ;
94379457 assert_eq ! ( result. stdout. trim( ) , "sourced" ) ;
94389458 }
9459+
9460+ #[ tokio:: test]
9461+ async fn test_internal_var_prefix_not_exposed ( ) {
9462+ // ${!_NAMEREF*} must not expose internal markers
9463+ let result = run_script ( "echo \" ${!_NAMEREF*}\" " ) . await ;
9464+ assert_eq ! ( result. stdout. trim( ) , "" ) ;
9465+ }
9466+
9467+ #[ tokio:: test]
9468+ async fn test_internal_var_readonly_not_exposed ( ) {
9469+ let result = run_script ( "echo \" ${!_READONLY*}\" " ) . await ;
9470+ assert_eq ! ( result. stdout. trim( ) , "" ) ;
9471+ }
9472+
9473+ #[ tokio:: test]
9474+ async fn test_internal_var_assignment_blocked ( ) {
9475+ // Direct assignment to _NAMEREF_ prefix should be silently ignored
9476+ let result = run_script ( "_NAMEREF_x=PATH; echo ${!x}" ) . await ;
9477+ assert ! ( !result. stdout. contains( "/usr" ) ) ;
9478+ }
9479+
9480+ #[ tokio:: test]
9481+ async fn test_internal_var_readonly_injection_blocked ( ) {
9482+ // Should not be able to fake readonly
9483+ let result = run_script ( "_READONLY_myvar=1; myvar=hello; echo $myvar" ) . await ;
9484+ assert_eq ! ( result. stdout. trim( ) , "hello" ) ;
9485+ }
94399486}
0 commit comments