@@ -556,10 +556,85 @@ pub struct OnBehalfOf {
556556}
557557
558558pub const ENTRYPOINT_OVERRIDE : & str = "_ENTRYPOINT_OVERRIDE" ;
559+
560+ /// The entrypoint override (`_ENTRYPOINT_OVERRIDE` job arg ->
561+ /// `v2_job.script_entrypoint_override`) is interpolated verbatim into
562+ /// generated worker wrappers in a code position (e.g. the NativeTS
563+ /// `import(...).then(m => m.<name>(...))` glue, the bun `Main.<name>(...)`
564+ /// call, the deno `import { <name> } from "./main.ts"` line, the PHP
565+ /// `<name>(...)` call and the Python `inner_script.<name>(**args)` call).
566+ /// A caller only needs `jobs:run` to set it, so it MUST be restricted to a
567+ /// conventional identifier or an attacker who can merely run a deployed
568+ /// script could break out of the call expression into arbitrary
569+ /// worker-process code. This ASCII subset is a valid function name in every
570+ /// language Windmill wraps this way (JS/TS, Python, PHP).
571+ pub fn is_valid_entrypoint_name ( name : & str ) -> bool {
572+ if name. is_empty ( ) || name. len ( ) > 255 {
573+ return false ;
574+ }
575+ let mut chars = name. chars ( ) ;
576+ let Some ( first) = chars. next ( ) else {
577+ return false ;
578+ } ;
579+ if !( first. is_ascii_alphabetic ( ) || first == '_' ) {
580+ return false ;
581+ }
582+ chars. all ( |c| c. is_ascii_alphanumeric ( ) || c == '_' )
583+ }
584+
559585pub const LARGE_LOG_THRESHOLD_SIZE : usize = 9000 ;
560586pub const EMAIL_ERROR_HANDLER_USER_EMAIL : & str = "email_error_handler@windmill.dev" ;
561587
562588#[ inline( always) ]
563589pub fn generate_dynamic_input_key ( workspace_id : & str , path : & str ) -> String {
564590 format ! ( "{workspace_id}:{path}" )
565591}
592+
593+ #[ cfg( test) ]
594+ mod tests {
595+ use super :: is_valid_entrypoint_name;
596+
597+ #[ test]
598+ fn valid_entrypoint_names_are_accepted ( ) {
599+ for name in [
600+ "main" ,
601+ "preprocessor" ,
602+ "my_helper" ,
603+ "_private" ,
604+ "fn2" ,
605+ "a" ,
606+ "MixedCase" ,
607+ ] {
608+ assert ! ( is_valid_entrypoint_name( name) , "expected {name:?} valid" ) ;
609+ }
610+ }
611+
612+ #[ test]
613+ fn malicious_entrypoint_names_are_rejected ( ) {
614+ // Regression for GHSA-wxjq-w5pj-jqhx: the entrypoint override is
615+ // interpolated verbatim into a code position of generated worker
616+ // wrappers (e.g. bun `Main.<name>(...)`, nativets
617+ // `m.<name>(...)`, python `inner_script.<name>(**args)`). Any value
618+ // that is not a strict identifier could break out of the call
619+ // expression into attacker-controlled worker code.
620+ for name in [
621+ "main(); globalThis.x = 1; //" , // breaks out of `Main.<name>(...)`
622+ "x); require('child_process').execSync('id'); (" ,
623+ "1main" , // starts with a digit
624+ "my-fn" , // hyphen
625+ "my fn" , // space
626+ "my.fn" , // member access
627+ "$fn" , // dollar
628+ "fn\n other" , // newline
629+ "fn;other" ,
630+ "" ,
631+ ] {
632+ assert ! (
633+ !is_valid_entrypoint_name( name) ,
634+ "expected {name:?} to be rejected"
635+ ) ;
636+ }
637+ // Over-long names are rejected.
638+ assert ! ( !is_valid_entrypoint_name( & "a" . repeat( 256 ) ) ) ;
639+ }
640+ }
0 commit comments