mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
fix: enforce jobs:run scope on job preview and inline endpoints (#9198)
* fix: enforce jobs:run scope on job preview and inline endpoints Preview/inline endpoints (run/preview, run/preview_bundle, run/preview_flow, run/dynamic_select inline) execute arbitrary request-supplied code but only checked folder/namespace read access, which is a no-op when path is null. A token scoped to a specific script/flow could escape its scope and run any code. Add a jobs:run scope check, matching other arbitrary-execution endpoints. Advisory GHSA-vxc5-w28p-m9xw. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: scope-check dynamic_select flow branch and inline preview Address CI review: the dynamic_select Deployed{Flow} branch ran a deployed flow's dynamic-select code without any scope check (only the Script branch delegated to a scope-checked handler), and run_inline_preview_script executed request-supplied code with no in-handler scope check. Add jobs:run:flows:{path} to the flow branch and jobs:run to inline preview; correct the misleading comment. Expand regression tests (preview_flow case, assert success for the broad-token case). Advisory GHSA-vxc5-w28p-m9xw. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: remove preview scope enforcement test after local validation The regression test passed locally (3/3) and validated the fix end-to-end; removed from the PR per maintainer preference. Advisory GHSA-vxc5-w28p-m9xw. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<String>,
|
||||
Json(preview): Json<PreviewInline>,
|
||||
) -> error::Result<Response> {
|
||||
// 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(),
|
||||
|
||||
Reference in New Issue
Block a user