mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 16:02:36 +00:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
124 lines
3.9 KiB
Rust
124 lines
3.9 KiB
Rust
//! Regression test for NUL bytes in draft values.
|
|
//!
|
|
//! `draft.value` is a `json` column (not `jsonb`), so a U+0000 escape can be
|
|
//! stored and then make any `->>`/`to_jsonb` extraction raise `22P05` — one
|
|
//! poisoned draft 500'd `GET /drafts/list` (silently hiding the home-page
|
|
//! "This workspace has N drafts" banner). The fix sanitizes the value on write
|
|
//! (`update_draft` -> `strip_json_nul`) so a NUL never reaches the column; this
|
|
//! drives the real endpoint and asserts the stored + listed value is NUL-free.
|
|
|
|
use serde_json::{json, Value};
|
|
use sqlx::{Pool, Postgres};
|
|
|
|
use windmill_test_utils::*;
|
|
|
|
fn client() -> reqwest::Client {
|
|
reqwest::Client::new()
|
|
}
|
|
|
|
fn authed(b: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
|
b.header("Authorization", "Bearer DNUL_ADMIN_TOKEN")
|
|
}
|
|
|
|
#[sqlx::test(fixtures("drafts_nul"))]
|
|
async fn test_draft_write_strips_nul(db: Pool<Postgres>) -> anyhow::Result<()> {
|
|
initialize_tracing().await;
|
|
let server = ApiServer::start(db.clone()).await?;
|
|
let port = server.addr.port();
|
|
let base = format!("http://localhost:{port}/api/w/dnul-ws");
|
|
|
|
// Save a draft whose summary and content carry a real NUL.
|
|
let resp = authed(client().post(format!(
|
|
"{base}/drafts/update/script/u/dnul-admin/poison"
|
|
)))
|
|
.json(&json!({
|
|
"value": {
|
|
"summary": "hi\u{0}there",
|
|
"path": "u/dnul-admin/poison",
|
|
"content": "x\u{0}y"
|
|
}
|
|
}))
|
|
.send()
|
|
.await?;
|
|
assert_eq!(
|
|
resp.status(),
|
|
200,
|
|
"save should succeed: {}",
|
|
resp.text().await.unwrap_or_default()
|
|
);
|
|
|
|
// The stored value must be NUL-free (sanitized on write).
|
|
let stored: Value = authed(client().get(format!(
|
|
"{base}/drafts/get_own/script/u/dnul-admin/poison"
|
|
)))
|
|
.send()
|
|
.await?
|
|
.json()
|
|
.await?;
|
|
let value = stored.get("value").expect("draft should exist");
|
|
assert_eq!(value["summary"], "hithere");
|
|
assert_eq!(value["content"], "xy");
|
|
assert!(
|
|
!serde_json::to_string(value).unwrap().contains("\\u0000"),
|
|
"stored value still contains a NUL escape: {value}"
|
|
);
|
|
|
|
// The list endpoint uses raw `->>`; it works (200, no 500) because the
|
|
// stored data is clean, and the summary comes back stripped.
|
|
let items: Vec<Value> = authed(client().get(format!("{base}/drafts/list")))
|
|
.send()
|
|
.await?
|
|
.json()
|
|
.await?;
|
|
let item = items
|
|
.iter()
|
|
.find(|d| d["path"] == "u/dnul-admin/poison")
|
|
.expect("saved draft should be listed");
|
|
assert_eq!(item["summary"], "hithere");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// The lineage the server derives for `draft.base` comes from the same value: read before
|
|
/// the sanitizer, a NUL in it costs the draft its base, which then reads as up to date
|
|
/// whatever the head is.
|
|
#[sqlx::test(fixtures("drafts_nul"))]
|
|
async fn test_draft_base_is_derived_from_sanitized_value(db: Pool<Postgres>) -> anyhow::Result<()> {
|
|
initialize_tracing().await;
|
|
let server = ApiServer::start(db.clone()).await?;
|
|
let port = server.addr.port();
|
|
let base = format!("http://localhost:{port}/api/w/dnul-ws");
|
|
|
|
let resp = authed(client().post(format!(
|
|
"{base}/drafts/update/script/u/dnul-admin/poisoned_base"
|
|
)))
|
|
.json(&json!({
|
|
"value": {
|
|
"summary": "s",
|
|
"path": "u/dnul-admin/poisoned_base",
|
|
"content": "x",
|
|
"parent_hash": "0000000000001b62\u{0}"
|
|
}
|
|
}))
|
|
.send()
|
|
.await?;
|
|
assert_eq!(
|
|
resp.status(),
|
|
200,
|
|
"save refused: {}",
|
|
resp.text().await.unwrap_or_default()
|
|
);
|
|
|
|
let stored: Option<String> = sqlx::query_scalar(
|
|
"SELECT base FROM draft WHERE workspace_id = 'dnul-ws' AND path = 'u/dnul-admin/poisoned_base'",
|
|
)
|
|
.fetch_one(&db)
|
|
.await?;
|
|
assert_eq!(
|
|
stored.as_deref(),
|
|
Some("0000000000001b62"),
|
|
"the NUL cost the draft its base: {stored:?}"
|
|
);
|
|
Ok(())
|
|
}
|