diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index dcec542cb8..231b62e234 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -5635,6 +5635,11 @@ async fn run_preview_script( "Operators cannot run preview jobs for security reasons".to_string(), )); } + // Preview runs arbitrary, request-supplied code. require_path_read_access_for_preview + // only checks folder/namespace *read* access (and is a no-op when path is null), so a + // token scoped to a specific script/flow could otherwise escape its scope and run any + // code. Require the broad jobs:run scope, like other arbitrary-execution endpoints. + check_scopes(&authed, || format!("jobs:run"))?; require_path_read_access_for_preview(&authed, &preview.path)?; let scheduled_for = run_query.get_scheduled_for(&db).await?; let tag = run_query.tag.clone().or(preview.tag.clone()); @@ -5716,6 +5721,9 @@ async fn run_inline_preview_script( Path(w_id): Path, Json(preview): Json, ) -> error::Result { + // Same arbitrary-code class as run_preview_script: a narrowly-scoped token + // must not be able to run request-supplied code through inline preview. + check_scopes(&authed, || format!("jobs:run"))?; if let Some(job_id) = job_id { register_potential_assets_on_inline_execution(job_id, &w_id, &preview); } @@ -5965,6 +5973,9 @@ async fn run_bundle_preview_script( "Operators cannot run preview jobs for security reasons".to_string(), )); } + // Bundle preview runs arbitrary, request-supplied code; require the broad jobs:run + // scope so a narrowly-scoped token cannot escape its scope. See run_preview_script. + check_scopes(&authed, || format!("jobs:run"))?; let mut job_id = None; let mut tx = None; @@ -6632,6 +6643,9 @@ async fn run_preview_flow_job( "Operators cannot run preview jobs for security reasons".to_string(), )); } + // Flow preview runs an arbitrary, request-supplied flow definition; require the broad + // jobs:run scope so a narrowly-scoped token cannot escape its scope. See run_preview_script. + check_scopes(&authed, || format!("jobs:run"))?; require_path_read_access_for_preview(&authed, &raw_flow.path)?; let scheduled_for = run_query.get_scheduled_for(&db).await?; let tag = run_query.tag.clone().or(raw_flow.tag.clone()); @@ -6779,6 +6793,11 @@ async fn run_dynamic_select( return Ok((StatusCode::CREATED, uuid.to_string()).into_response()); } RunnableKind::Flow => { + // Runs the deployed flow's dynamic-select code. Enforce the same + // path-scoped check the script branch gets via + // push_script_job_by_path_into_queue, so a token not scoped to this + // flow cannot trigger its code through dynamic select. + check_scopes(&authed, || format!("jobs:run:flows:{path}"))?; let mut conn = user_db.clone().begin(&authed).await?; let dynamic_input_res = match DYNAMIC_INPUT_CACHE.get(&format!("{}:{}", w_id, path)) @@ -6826,6 +6845,11 @@ async fn run_dynamic_select( } }, DynamicSelectRunnableRef::Inline { code, lang: language } => { + // Inline dynamic select runs arbitrary, request-supplied code; require the broad + // jobs:run scope so a narrowly-scoped token cannot escape its scope. The Deployed + // branches are path-scoped instead (scripts via push_script_job_by_path_into_queue, + // flows via the check_scopes above). + check_scopes(&authed, || format!("jobs:run"))?; dynamic_input = DynamicInput { x_windmill_dyn_select_code: code, x_windmill_dyn_select_lang: language.unwrap_or_default(),