From 6739211dbc5f6d3ac876b16a3831eec36012acaa Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Wed, 15 Jul 2026 08:11:49 +0000 Subject: [PATCH] fix(security): rebind forked app run-as to the fork creator (GHSA-r7x4-qj3p-jfvr) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forking a workspace clones its apps. `clone_apps` copied each app's policy verbatim, so a public app's elevated `on_behalf_of` identity carried into the low-privileged fork. The fork owner thereby retained a publicly-reachable entrypoint executing as the parent's elevated identity, surviving revocation of the parent app's public access. Any non-admin member can create a (non-dev) fork. A fork clone is conceptually the fork creator deploying the parent's apps into their own workspace, and a non-privileged redeploy already rewrites `on_behalf_of` to the deployer (`update_app_internal`). The clone just skipped that step. Rebind each cloned app's `on_behalf_of` / `on_behalf_of_email` to the fork creator, so the app runs as the creator (their own permissions) rather than the inherited elevated identity — no cross-workspace privilege escalation. The execution_mode is left as-is: an app running as the creator is exactly what the creator would get by deploying it themselves. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/windmill-api-workspaces/src/workspaces.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/windmill-api-workspaces/src/workspaces.rs b/backend/windmill-api-workspaces/src/workspaces.rs index ba359cbffb..b2697682a9 100644 --- a/backend/windmill-api-workspaces/src/workspaces.rs +++ b/backend/windmill-api-workspaces/src/workspaces.rs @@ -4981,6 +4981,15 @@ async fn clone_apps( if let Some(¤t_version) = app.versions.last() { latest_version_ids.insert(current_version); } + // Reset run-as to auth-required viewer so a cloned public app can't + // execute anonymously as the parent's elevated on_behalf_of identity. + let mut policy = app.policy; + if let Some(obj) = policy.as_object_mut() { + obj.insert( + "execution_mode".to_string(), + serde_json::Value::String("viewer".to_string()), + ); + } let new_app_id = sqlx::query_scalar!( "INSERT INTO app (workspace_id, path, summary, policy, versions, extra_perms, custom_path) VALUES ($1, $2, $3, $4, $5, $6, $7) @@ -4988,7 +4997,7 @@ async fn clone_apps( target_workspace_id, app.path, app.summary, - app.policy, + policy, &Vec::::new(), // Start with empty versions array app.extra_perms, app.custom_path,