@@ -413,8 +413,8 @@ stream(Module, Func, Args, Kwargs) when map_size(Kwargs) == 0 ->
413413stream (Module , Func , Args , Kwargs ) ->
414414 % % With kwargs - use eval approach
415415 Ctx = py_context_router :get_context (),
416- ModuleBin = ensure_binary (Module ),
417- FuncBin = ensure_binary (Func ),
416+ ModuleBin = valid_py_module ( ensure_binary (Module ) ),
417+ FuncBin = valid_py_ident ( ensure_binary (Func ) ),
418418 KwargsCode = format_kwargs (Kwargs ),
419419 ArgsCode = format_args (Args ),
420420 Code = iolist_to_binary ([
@@ -445,16 +445,16 @@ format_args(Args) ->
445445% % @private Format a single argument
446446format_arg (A ) when is_integer (A ) -> integer_to_binary (A );
447447format_arg (A ) when is_float (A ) -> float_to_binary (A );
448- format_arg (A ) when is_binary (A ) -> <<" '" , A /binary , " '" >>;
449- format_arg (A ) when is_atom (A ) -> <<" '" , (atom_to_binary (A ))/binary , " '" >>;
448+ format_arg (A ) when is_binary (A ) -> <<" '" , ( escape_py_literal ( A )) /binary , " '" >>;
449+ format_arg (A ) when is_atom (A ) -> <<" '" , (escape_py_literal ( atom_to_binary (A ) ))/binary , " '" >>;
450450format_arg (A ) when is_list (A ) -> iolist_to_binary ([<<" [" >>, format_args (A ), <<" ]" >>]);
451451format_arg (_ ) -> <<" None" >>.
452452
453453% % @private Format kwargs for Python code
454454format_kwargs (Kwargs ) when map_size (Kwargs ) == 0 -> <<>>;
455455format_kwargs (Kwargs ) ->
456456 KwList = maps :fold (fun (K , V , Acc ) ->
457- KB = if is_atom (K ) -> atom_to_binary (K ); is_binary (K ) -> K end ,
457+ KB = valid_py_ident ( if is_atom (K ) -> atom_to_binary (K ); is_binary (K ) -> K end ) ,
458458 [<<KB /binary , " =" , (format_arg (V ))/binary >> | Acc ]
459459 end , [], Kwargs ),
460460 iolist_to_binary ([<<" , " >>, lists :join (<<" , " >>, KwList )]).
@@ -543,7 +543,9 @@ stream_start(Module, Func, Args, Opts) ->
543543 {ok , Ref }.
544544
545545% % @private Run the streaming via Python code
546- stream_run_python (ModuleBin , FuncBin , RefHash ) ->
546+ stream_run_python (ModuleBin0 , FuncBin0 , RefHash ) ->
547+ ModuleBin = valid_py_module (ModuleBin0 ),
548+ FuncBin = valid_py_ident (FuncBin0 ),
547549 RefHashBin = integer_to_binary (RefHash ),
548550 % % Build Python code that streams values using callbacks
549551 Code = iolist_to_binary ([
@@ -1070,6 +1072,51 @@ escape_python_string(Str) ->
10701072 (C ) -> [C ]
10711073 end , Str ).
10721074
1075+ % % @private Escape a binary for safe embedding inside a single-quoted Python
1076+ % % string literal: quote, backslash, and newline/CR/tab/other control bytes that
1077+ % % would otherwise break out of or corrupt the literal.
1078+ escape_py_literal (Bin ) when is_binary (Bin ) ->
1079+ << <<(escape_py_byte (B ))/binary >> || <<B >> <= Bin >>.
1080+
1081+ escape_py_byte ($' ) -> <<" \\ '" >>;
1082+ escape_py_byte ($\\ ) -> <<" \\\\ " >>;
1083+ escape_py_byte ($\n ) -> <<" \\ n" >>;
1084+ escape_py_byte ($\r ) -> <<" \\ r" >>;
1085+ escape_py_byte ($\t ) -> <<" \\ t" >>;
1086+ escape_py_byte (B ) when B < 16#20 ; B =:= 16#7f ->
1087+ list_to_binary (io_lib :format (" \\ x~2.16.0b " , [B ]));
1088+ escape_py_byte (B ) -> <<B >>.
1089+
1090+ % % @private Validate a Python identifier ([A-Za-z_][A-Za-z0-9_]*). Crashes on a
1091+ % % non-conforming value so an attacker-controlled module/func/kwarg name can't
1092+ % % inject code at an identifier position (where quoting is meaningless).
1093+ valid_py_ident (Bin ) when is_binary (Bin ), byte_size (Bin ) > 0 ->
1094+ case ident_ok (Bin , first ) of
1095+ true -> Bin ;
1096+ false -> error ({invalid_python_identifier , Bin })
1097+ end ;
1098+ valid_py_ident (Other ) ->
1099+ error ({invalid_python_identifier , Other }).
1100+
1101+ % % @private Validate a dotted Python module path (each segment an identifier).
1102+ valid_py_module (Bin ) when is_binary (Bin ), byte_size (Bin ) > 0 ->
1103+ Segments = binary :split (Bin , <<" ." >>, [global ]),
1104+ lists :foreach (fun valid_py_ident /1 , Segments ),
1105+ Bin ;
1106+ valid_py_module (Other ) ->
1107+ error ({invalid_python_identifier , Other }).
1108+
1109+ ident_ok (<<>>, first ) -> false ; % % empty segment (leading/trailing/double dot)
1110+ ident_ok (<<>>, rest ) -> true ;
1111+ ident_ok (<<C , Rest /binary >>, first )
1112+ when (C >= $A andalso C =< $Z ); (C >= $a andalso C =< $z ); C =:= $_ ->
1113+ ident_ok (Rest , rest );
1114+ ident_ok (<<C , Rest /binary >>, rest )
1115+ when (C >= $A andalso C =< $Z ); (C >= $a andalso C =< $z );
1116+ (C >= $0 andalso C =< $9 ); C =:= $_ ->
1117+ ident_ok (Rest , rest );
1118+ ident_ok (_ , _ ) -> false .
1119+
10731120% % @doc Deactivate the current virtual environment.
10741121% % Restores sys.path to its original state.
10751122-spec deactivate_venv () -> ok | {error , term ()}.
@@ -1262,13 +1309,13 @@ configure_logging(Opts) ->
12621309 iolist_to_binary ([
12631310 " __import__('erlang').setup_logging(" ,
12641311 integer_to_binary (LevelInt ),
1265- " , '" , F , " ')"
1312+ " , '" , escape_py_literal ( F ) , " ')"
12661313 ]);
12671314 F when is_list (F ) ->
12681315 iolist_to_binary ([
12691316 " __import__('erlang').setup_logging(" ,
12701317 integer_to_binary (LevelInt ),
1271- " , '" , F , " ')"
1318+ " , '" , escape_py_literal ( iolist_to_binary ( F )) , " ')"
12721319 ])
12731320 end ,
12741321 case eval (Code ) of
0 commit comments