@@ -6037,11 +6037,17 @@ impl Interpreter {
60376037 . variables
60386038 . keys ( )
60396039 . filter ( |k| k. starts_with ( prefix. as_str ( ) ) )
6040+ // THREAT[TM-INJ-009]: Hide internal marker variables
6041+ . filter ( |k| !Self :: is_internal_variable ( k) )
60406042 . cloned ( )
60416043 . collect ( ) ;
60426044 // Also check env
60436045 for k in self . env . keys ( ) {
6044- if k. starts_with ( prefix. as_str ( ) ) && !names. contains ( k) {
6046+ if k. starts_with ( prefix. as_str ( ) )
6047+ && !names. contains ( k)
6048+ // THREAT[TM-INJ-009]: Hide internal marker variables
6049+ && !Self :: is_internal_variable ( k)
6050+ {
60456051 names. push ( k. clone ( ) ) ;
60466052 }
60476053 }
@@ -7519,10 +7525,24 @@ impl Interpreter {
75197525 s. to_string ( )
75207526 }
75217527
7528+ /// THREAT[TM-INJ-009]: Check if a variable name is an internal marker.
7529+ fn is_internal_variable ( name : & str ) -> bool {
7530+ name. starts_with ( "_NAMEREF_" )
7531+ || name. starts_with ( "_READONLY_" )
7532+ || name. starts_with ( "_UPPER_" )
7533+ || name. starts_with ( "_LOWER_" )
7534+ || name == "_SHIFT_COUNT"
7535+ || name == "_SET_POSITIONAL"
7536+ }
7537+
75227538 /// Set a variable, respecting dynamic scoping.
75237539 /// If the variable is declared `local` in any active call frame, update that frame.
75247540 /// Otherwise, set in global variables.
75257541 fn set_variable ( & mut self , name : String , value : String ) {
7542+ // THREAT[TM-INJ-009]: Block user assignment to internal marker variables
7543+ if Self :: is_internal_variable ( & name) {
7544+ return ;
7545+ }
75267546 // Resolve nameref: if `name` is a nameref, assign to the target instead
75277547 let resolved = self . resolve_nameref ( & name) . to_string ( ) ;
75287548 // Apply case conversion attributes (declare -l / declare -u)
@@ -9374,4 +9394,31 @@ mod tests {
93749394 let result = run_script ( "echo $(( 9223372036854775807 * 2 ))" ) . await ;
93759395 assert_eq ! ( result. exit_code, 0 ) ;
93769396 }
9397+
9398+ #[ tokio:: test]
9399+ async fn test_internal_var_prefix_not_exposed ( ) {
9400+ // ${!_NAMEREF*} must not expose internal markers
9401+ let result = run_script ( "echo \" ${!_NAMEREF*}\" " ) . await ;
9402+ assert_eq ! ( result. stdout. trim( ) , "" ) ;
9403+ }
9404+
9405+ #[ tokio:: test]
9406+ async fn test_internal_var_readonly_not_exposed ( ) {
9407+ let result = run_script ( "echo \" ${!_READONLY*}\" " ) . await ;
9408+ assert_eq ! ( result. stdout. trim( ) , "" ) ;
9409+ }
9410+
9411+ #[ tokio:: test]
9412+ async fn test_internal_var_assignment_blocked ( ) {
9413+ // Direct assignment to _NAMEREF_ prefix should be silently ignored
9414+ let result = run_script ( "_NAMEREF_x=PATH; echo ${!x}" ) . await ;
9415+ assert ! ( !result. stdout. contains( "/usr" ) ) ;
9416+ }
9417+
9418+ #[ tokio:: test]
9419+ async fn test_internal_var_readonly_injection_blocked ( ) {
9420+ // Should not be able to fake readonly
9421+ let result = run_script ( "_READONLY_myvar=1; myvar=hello; echo $myvar" ) . await ;
9422+ assert_eq ! ( result. stdout. trim( ) , "hello" ) ;
9423+ }
93779424}
0 commit comments