mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
066d7a4726
`POST /api/w/{workspace}/jobs/run_inline/preview` ran request-supplied
code inline (in-process, e.g. DuckDB) but was missing the operator
authorization guard that its sibling `/jobs/run/preview` enforces. An
authenticated operator — the most restricted role, which must not run
preview jobs — could execute arbitrary code in a single request
(file read/write, and OS command execution via the DuckDB `shellfs`
extension when worker egress is available).
This is the incomplete-fix residual of CVE-2026-22683 / GHSA-9q9g-rp9x-244h,
whose v1.615.0 patch covered the entity-CRUD endpoints but left this
direct inline-exec sink uncovered.
Add the same `is_operator` guard from `run_preview_script`. Audited the
rest of the preview/inline arbitrary-code endpoints (run_preview_script,
run_bundle_preview_script, run_preview_flow_job, the wait_result
wrappers, run_dynamic_select inline variant, dependency jobs) — all
already carry the guard. The `run_inline_script_by_path`/`by_hash`
endpoints run deployed scripts (operator-allowed, scope-checked) and
correctly remain ungated.
Fixes WIN-2043 (GHSA-pp5h-96x3-3wqq).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
3.7 KiB
Rust
91 lines
3.7 KiB
Rust
//! Regression test for the inline preview authorization bypass (GHSA-pp5h-96x3-3wqq).
|
|
//!
|
|
//! `POST /api/w/:workspace/jobs/run_inline/preview` -> `run_inline_preview_script`
|
|
//! runs request-supplied code inline (in-process via DuckDB), i.e. it is an
|
|
//! arbitrary-code-execution sibling of `/jobs/run/preview`. The bug was that
|
|
//! this handler was missing the Operator guard that `run_preview_script`
|
|
//! enforces, so an authenticated Operator (a run-only user who must not be able
|
|
//! to run preview jobs) could execute arbitrary code in a single request. This
|
|
//! was the incomplete-fix residual of CVE-2026-22683, whose v1.615.0 patch only
|
|
//! covered the entity-CRUD endpoints and left this direct inline-exec sink open.
|
|
//!
|
|
//! This test pins down:
|
|
//! - an Operator is rejected by the operator guard (the core fix; pre-fix this
|
|
//! reached the inline executor instead of returning 401), and
|
|
//! - a regular non-operator passes the guard (the fix must not over-block the
|
|
//! legitimate inline preview flow): in the test harness the worker inline
|
|
//! utils are not registered, so a caller past the guard gets the distinct
|
|
//! "worker inline functions" error rather than the operator rejection.
|
|
|
|
use serde_json::json;
|
|
use sqlx::{Pool, Postgres};
|
|
use windmill_test_utils::*;
|
|
|
|
fn client() -> reqwest::Client {
|
|
reqwest::Client::new()
|
|
}
|
|
|
|
fn authed(builder: reqwest::RequestBuilder, token: &str) -> reqwest::RequestBuilder {
|
|
builder.header("Authorization", format!("Bearer {}", token))
|
|
}
|
|
|
|
/// An inline preview request: request-supplied `content` to run via DuckDB.
|
|
fn inline_preview_body() -> serde_json::Value {
|
|
json!({
|
|
"language": "duckdb",
|
|
"content": "SELECT content FROM read_text(['/etc/passwd']);",
|
|
"args": {}
|
|
})
|
|
}
|
|
|
|
const OPERATOR_GUARD_MSG: &str = "Operators cannot run preview jobs";
|
|
|
|
#[sqlx::test(fixtures("base", "inline_preview_auth"))]
|
|
async fn test_inline_preview_authorization(db: Pool<Postgres>) -> anyhow::Result<()> {
|
|
initialize_tracing().await;
|
|
|
|
let server = ApiServer::start(db.clone()).await?;
|
|
let port = server.addr.port();
|
|
let url = format!("http://localhost:{port}/api/w/test-workspace/jobs/run_inline/preview");
|
|
|
|
// 1. CORE REGRESSION: an Operator must be rejected by the operator guard.
|
|
// Pre-fix this fell through to the inline executor (arbitrary code
|
|
// execution); post-fix it returns 401 with the operator guard message.
|
|
let resp = authed(client().post(&url), "OPERATOR_TOKEN")
|
|
.json(&inline_preview_body())
|
|
.send()
|
|
.await?;
|
|
let status = resp.status();
|
|
let body = resp.text().await?;
|
|
assert_eq!(
|
|
status, 401,
|
|
"Operator must be rejected from inline preview (got {status}): {body}"
|
|
);
|
|
assert!(
|
|
body.contains(OPERATOR_GUARD_MSG),
|
|
"rejection must be the operator guard, got: {body}"
|
|
);
|
|
|
|
// 2. The fix must NOT over-block a legitimate non-operator: a regular member
|
|
// passes the operator + scope checks. The test harness does not register
|
|
// the worker inline utils, so the request proceeds past the guard and
|
|
// fails later with the distinct "worker inline functions" error — proving
|
|
// the operator guard did not reject it.
|
|
let resp = authed(client().post(&url), "SECRET_TOKEN_2")
|
|
.json(&inline_preview_body())
|
|
.send()
|
|
.await?;
|
|
let status = resp.status();
|
|
let body = resp.text().await?;
|
|
assert_ne!(
|
|
status, 401,
|
|
"non-operator must not be blocked by the operator guard (got {status}): {body}"
|
|
);
|
|
assert!(
|
|
!body.contains(OPERATOR_GUARD_MSG),
|
|
"non-operator must not hit the operator guard, got: {body}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|