[ee] fix: stop handlers, apps and agents resolving untrusted refs (#11893)

* fix: stop handler, app and agent jobs resolving untrusted references

* perf: skip decoding untrusted args that hold no dollar or escape

* test: drop the transaction the recovery handler no longer takes

* fix: flag reserved names in object keys and tighten the jsonb parity test
This commit is contained in:
AlexRV12
2026-10-21 10:19:20 +02:00
committed by GitHub
parent f400d520d6
commit eda610b2f4
8 changed files with 627 additions and 42 deletions
+3 -6
View File
@@ -21,7 +21,7 @@ use windmill_common::{
jobs::JobPayload,
users::username_to_permissioned_as,
utils::{not_found_if_none, StripPath},
variables::arg_needs_interpolation,
variables::untrusted_arg_interpolates,
};
use windmill_queue::{push, PushArgs, PushIsolationLevel};
@@ -136,11 +136,8 @@ pub(crate) async fn run_agent_as(
};
if let Some(obo) = on_behalf_of.as_ref() {
// The worker would interpolate such an input with the job's token, which is the
// identity's. Checked on the decoded value: the raw JSON can spell `$` as `\u0024`.
if args.values().any(|v| {
serde_json::from_str::<serde_json::Value>(v.get())
.is_ok_and(|v| arg_needs_interpolation(&v.to_string()))
}) {
// identity's.
if args.values().any(|v| untrusted_arg_interpolates(v.get())) {
return Err(Error::BadRequest(format!(
"Agent {path} runs on behalf of {}, so its inputs cannot reference variables or resources",
obo.permissioned_as
+9 -23
View File
@@ -76,7 +76,7 @@ use windmill_common::{
check_proper_path, http_get_from_hub, not_found_if_none, paginate, paginate_optional,
query_elems_from_hub, require_admin, strip_json_nul, Pagination, RunnableKind, StripPath,
},
variables::{build_crypt, build_crypt_with_key_suffix, encrypt},
variables::{build_crypt, build_crypt_with_key_suffix, encrypt, omit_untrusted_interpolation},
worker::{to_raw_value, CLOUD_HOSTED},
workspaces::{
check_deploy_rules, check_operator_can_build, check_user_against_rule,
@@ -6553,6 +6553,7 @@ async fn build_args(
}
}
let mut untrusted = HashMap::new();
for (k, v) in args {
if safe_args.contains_key(&k) {
continue;
@@ -6566,31 +6567,16 @@ async fn build_args(
k.to_string(),
ctx_value(prop, policy, authed, w_id, db).await?,
);
} else if !arg_str.contains("\"$var:") && !arg_str.contains("\"$res:") {
safe_args.insert(k.to_string(), v);
} else {
safe_args.insert(
k.to_string(),
RawValue::from_string(
arg_str
.replace(
"$var:",
"The following variable has been omitted for security reasons: ",
)
.replace(
"$res:",
"The following resource has been omitted for security reasons, to allow it, toggle: 'Allow resources from users' on that field input: ",
),
)
.map_err(|e| {
Error::internal_err(format!(
"failed to remove sensitive variable(s)/resource(s) with error: {}",
e
))
})?,
);
untrusted.insert(k, v);
}
}
safe_args.extend(
omit_untrusted_interpolation(
untrusted,
"The following resource has been omitted for security reasons, to allow it, toggle: 'Allow resources from users' on that field input: ",
),
);
for k in sensitive_inputs {
let Some(v) = safe_args.get(k) else { continue };
let raw = v.get();