mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-26 08:01:38 +00:00
fix(apps): deny preview-flow restart replay for raw app SDK tokens
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
This commit is contained in:
@@ -743,13 +743,16 @@ pub fn has_raw_app_sdk_sentinel(scopes: Option<&[String]>) -> bool {
|
||||
/// Without this, a captured SDK token escalates to the viewer's full account:
|
||||
/// these jobs run attacker-chosen code (or resolve and install attacker-chosen
|
||||
/// packages on a worker) and their own ephemeral credential is unscoped and
|
||||
/// permissioned as the viewer. `workflow_as_code` counts even though its body is
|
||||
/// just args: it copies the named queued job's `raw_code` into a new job without
|
||||
/// an ownership check, so a pre-staged preview job replays as the viewer.
|
||||
/// permissioned as the viewer. The last two count even though their bodies carry
|
||||
/// no code, because they re-run code a *named* job already holds:
|
||||
/// `workflow_as_code` copies the named queued job's `raw_code` into a new job
|
||||
/// with no ownership check, and restarting a completed preview flow re-runs its
|
||||
/// stored `raw_flow`. Either replays a pre-staged preview as the viewer, and the
|
||||
/// scope layer can't tell a preview restart from a deployed-flow one.
|
||||
/// The SDK never calls these — its run helpers are all by path/hash.
|
||||
fn is_request_supplied_code_route(suffix: &str) -> bool {
|
||||
// Prefixes, so the `_async` variants are covered too.
|
||||
const CODE_ROUTES: [&str; 9] = [
|
||||
const CODE_ROUTES: [&str; 10] = [
|
||||
"jobs/run/preview",
|
||||
"jobs/run_inline/preview",
|
||||
"jobs/run_wait_result/preview",
|
||||
@@ -759,6 +762,7 @@ fn is_request_supplied_code_route(suffix: &str) -> bool {
|
||||
"jobs/run/dependencies",
|
||||
"jobs/run/flow_dependencies",
|
||||
"jobs/run/workflow_as_code",
|
||||
"jobs/restart/f",
|
||||
];
|
||||
CODE_ROUTES.iter().any(|p| suffix.starts_with(p))
|
||||
}
|
||||
|
||||
@@ -4918,12 +4918,13 @@ mod embed_token_tests {
|
||||
.includes(&required));
|
||||
}
|
||||
|
||||
/// The raw-app frontend SDK token is bounded by two properties: no `apps:*`
|
||||
/// scope (so the Apps-domain mint endpoints are unreachable and a captured
|
||||
/// token can't renew itself past its expiry), and the `raw_app_sdk` sentinel
|
||||
/// (so `jobs:run` can't reach the request-supplied-code endpoints, which
|
||||
/// would escalate a captured token to arbitrary execution as the viewer).
|
||||
/// Lock both, plus the viewer-permissioned surface each curated scope grants.
|
||||
/// The raw-app frontend SDK token is bounded by no `apps:*` scope (so the
|
||||
/// Apps-domain mint endpoints are unreachable and a captured token can't renew
|
||||
/// itself past its expiry) and by the `raw_app_sdk` sentinel (so `jobs:run`
|
||||
/// can't reach the endpoints that run caller-supplied or caller-named code —
|
||||
/// which would escalate a captured token to arbitrary execution as the viewer
|
||||
/// — and `users:read` can't reach the workspace member directory).
|
||||
/// Lock all of it, plus the viewer-permissioned surface each curated scope grants.
|
||||
#[test]
|
||||
fn frontend_sdk_scopes_reach_declared_domains_but_never_mint_routes() {
|
||||
// Mirror mint_raw_app_sdk_token: declared scopes + the narrowing sentinel.
|
||||
@@ -4981,11 +4982,13 @@ mod embed_token_tests {
|
||||
("/api/w/test/jobs/run/dependencies_async", "POST"),
|
||||
("/api/w/test/jobs/run/flow_dependencies", "POST"),
|
||||
("/api/w/test/jobs/run/flow_dependencies_async", "POST"),
|
||||
// Replays a named queued preview job's raw code as the caller.
|
||||
// Replay a named job's already-stored code as the caller: the queued
|
||||
// preview's raw_code, or a completed preview flow's raw_flow.
|
||||
(
|
||||
"/api/w/test/jobs/run/workflow_as_code/some-uuid/main",
|
||||
"POST",
|
||||
),
|
||||
("/api/w/test/jobs/restart/f/some-uuid", "POST"),
|
||||
// `users:read` is presented to the viewer as "read your identity", so
|
||||
// the workspace member directory must stay out of reach.
|
||||
("/api/w/test/users/list", "GET"),
|
||||
|
||||
Reference in New Issue
Block a user