use crate::args_error::ToolsetArgsError;
pub const ARGS_KEY_DENYLIST: &[&str] = &[
"toJSON",
"then",
"__proto__",
"constructor",
"prototype",
"toString",
"valueOf",
"__defineGetter__",
"__defineSetter__",
"__lookupGetter__",
"__lookupSetter__",
];
pub const TOOLSET_ARGS_MAX_DEPTH: usize = 16;
pub const TOOLSET_ARGS_MAX_NODES: usize = 1_024;
pub fn validate_toolset_tool_args(args: &serde_json::Value) -> Result<(), ToolsetArgsError> {
let mut stack: Vec<(&serde_json::Value, usize)> = Vec::new();
stack.push((args, 0));
let mut nodes_visited: usize = 0;
while let Some((value, depth)) = stack.pop() {
nodes_visited = nodes_visited.saturating_add(1);
if nodes_visited > TOOLSET_ARGS_MAX_NODES {
return Err(ToolsetArgsError::TooManyNodes {
count_limit: TOOLSET_ARGS_MAX_NODES,
});
}
if depth > TOOLSET_ARGS_MAX_DEPTH {
return Err(ToolsetArgsError::NestingTooDeep {
depth,
max_depth: TOOLSET_ARGS_MAX_DEPTH,
});
}
match value {
serde_json::Value::Object(map) => {
for (key, child) in map {
if let Some(matched) = denylist_match(key.as_str()) {
return Err(ToolsetArgsError::DangerousKey {
matched_key: matched,
});
}
stack.push((child, depth + 1));
}
}
serde_json::Value::Array(elements) => {
for element in elements {
stack.push((element, depth + 1));
}
}
_ => {}
}
}
Ok(())
}
#[inline]
fn denylist_match(key: &str) -> Option<&'static str> {
ARGS_KEY_DENYLIST
.iter()
.copied()
.find(|&denied| key == denied)
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "test-only; panics and unwraps acceptable in unit tests"
)]
use serde_json::{Value, json};
use super::*;
fn assert_dangerous_key(result: &Result<(), ToolsetArgsError>, expected_key: &str) {
match result {
Err(ToolsetArgsError::DangerousKey { matched_key }) => {
assert_eq!(
*matched_key, expected_key,
"expected matched_key = {expected_key:?}, got {matched_key:?}"
);
let display = result.as_ref().unwrap_err().to_string();
assert!(
display.contains(expected_key),
"Display must mention the matched constant: {display}"
);
}
other => panic!("expected DangerousKey({expected_key}), got {other:?}"),
}
}
#[test]
fn benign_flat_object_passes() {
let val = json!({
"account_id": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
"chain_id": "stellar:testnet"
});
validate_toolset_tool_args(&val).unwrap();
}
#[test]
fn benign_null_passes() {
validate_toolset_tool_args(&Value::Null).unwrap();
}
#[test]
fn benign_string_passes() {
validate_toolset_tool_args(&Value::String("hello".into())).unwrap();
}
#[test]
fn benign_array_of_objects_passes() {
let val = json!([
{ "account_id": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN" },
{ "chain_id": "stellar:testnet" }
]);
validate_toolset_tool_args(&val).unwrap();
}
#[test]
fn benign_nested_at_max_depth_passes() {
let mut val = json!({ "leaf": "value" });
for _ in 0..TOOLSET_ARGS_MAX_DEPTH - 1 {
val = json!({ "nested": val });
}
validate_toolset_tool_args(&val).unwrap();
}
#[test]
fn denylist_tojson_top_level() {
let val = json!({ "toJSON": "something" });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "toJSON");
}
#[test]
fn denylist_then_top_level() {
let val = json!({ "then": "something" });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "then");
}
#[test]
fn denylist_proto_top_level() {
let val = json!({ "__proto__": { "isAdmin": true } });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__proto__");
}
#[test]
fn denylist_constructor_top_level() {
let val = json!({ "constructor": "something" });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "constructor");
}
#[test]
fn denylist_prototype_top_level() {
let val = json!({ "prototype": "something" });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "prototype");
}
#[test]
fn denylist_tostring_top_level() {
let val = json!({ "toString": "something" });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "toString");
}
#[test]
fn denylist_valueof_top_level() {
let val = json!({ "valueOf": "something" });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "valueOf");
}
#[test]
fn denylist_define_getter_top_level() {
let val = json!({ "__defineGetter__": "something" });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__defineGetter__");
}
#[test]
fn denylist_define_setter_top_level() {
let val = json!({ "__defineSetter__": "something" });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__defineSetter__");
}
#[test]
fn denylist_lookup_getter_top_level() {
let val = json!({ "__lookupGetter__": "something" });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__lookupGetter__");
}
#[test]
fn denylist_lookup_setter_top_level() {
let val = json!({ "__lookupSetter__": "something" });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__lookupSetter__");
}
#[test]
fn denylist_tojson_nested_in_object() {
let val = json!({ "safe_key": { "toJSON": "evil" } });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "toJSON");
}
#[test]
fn denylist_then_nested_in_object() {
let val = json!({ "outer": { "inner": { "then": "evil" } } });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "then");
}
#[test]
fn denylist_proto_nested_in_object() {
let val = json!({ "metadata": { "__proto__": "evil" } });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__proto__");
}
#[test]
fn denylist_constructor_nested_in_object() {
let val = json!({ "outer": { "constructor": "evil" } });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "constructor");
}
#[test]
fn denylist_prototype_nested_in_object() {
let val = json!({ "outer": { "prototype": "evil" } });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "prototype");
}
#[test]
fn denylist_tostring_nested_in_object() {
let val = json!({ "outer": { "toString": "evil" } });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "toString");
}
#[test]
fn denylist_valueof_nested_in_object() {
let val = json!({ "outer": { "valueOf": "evil" } });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "valueOf");
}
#[test]
fn denylist_define_getter_nested_in_object() {
let val = json!({ "outer": { "__defineGetter__": "evil" } });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__defineGetter__");
}
#[test]
fn denylist_define_setter_nested_in_object() {
let val = json!({ "outer": { "__defineSetter__": "evil" } });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__defineSetter__");
}
#[test]
fn denylist_lookup_getter_nested_in_object() {
let val = json!({ "outer": { "__lookupGetter__": "evil" } });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__lookupGetter__");
}
#[test]
fn denylist_lookup_setter_nested_in_object() {
let val = json!({ "outer": { "__lookupSetter__": "evil" } });
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__lookupSetter__");
}
#[test]
fn denylist_tojson_nested_in_array() {
let val = json!([{ "toJSON": "evil" }]);
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "toJSON");
}
#[test]
fn denylist_then_nested_in_array() {
let val = json!([{ "benign": "value" }, { "then": "evil" }]);
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "then");
}
#[test]
fn denylist_proto_nested_in_array() {
let val = json!([{ "__proto__": "evil" }]);
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__proto__");
}
#[test]
fn denylist_constructor_nested_in_array() {
let val = json!([{ "constructor": "evil" }]);
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "constructor");
}
#[test]
fn denylist_prototype_nested_in_array() {
let val = json!([{ "prototype": "evil" }]);
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "prototype");
}
#[test]
fn denylist_tostring_nested_in_array() {
let val = json!([{ "toString": "evil" }]);
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "toString");
}
#[test]
fn denylist_valueof_nested_in_array() {
let val = json!([{ "valueOf": "evil" }]);
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "valueOf");
}
#[test]
fn denylist_define_getter_nested_in_array() {
let val = json!([{ "__defineGetter__": "evil" }]);
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__defineGetter__");
}
#[test]
fn denylist_define_setter_nested_in_array() {
let val = json!([{ "__defineSetter__": "evil" }]);
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__defineSetter__");
}
#[test]
fn denylist_lookup_getter_nested_in_array() {
let val = json!([{ "__lookupGetter__": "evil" }]);
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__lookupGetter__");
}
#[test]
fn denylist_lookup_setter_nested_in_array() {
let val = json!([{ "__lookupSetter__": "evil" }]);
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__lookupSetter__");
}
#[test]
fn depth_at_max_plus_1_rejected() {
let mut val = json!({ "leaf": "value" });
for _ in 0..=TOOLSET_ARGS_MAX_DEPTH {
val = json!({ "nested": val });
}
let r = validate_toolset_tool_args(&val);
assert!(
matches!(r, Err(ToolsetArgsError::NestingTooDeep { .. })),
"expected NestingTooDeep, got {r:?}"
);
}
#[test]
fn depth_at_serde_parse_limit_no_overflow() {
let depth = 500_usize;
let mut val = json!({ "leaf": "value" });
for _ in 0..depth {
val = json!({ "nested": val });
}
let r = validate_toolset_tool_args(&val);
assert!(
matches!(r, Err(ToolsetArgsError::NestingTooDeep { .. })),
"expected NestingTooDeep for depth-{depth} value, got {r:?}"
);
}
#[test]
fn proto_unicode_escaped_caught_as_literal() {
let json_str = r#"{ "__proto__": "evil" }"#;
let val: serde_json::Value = serde_json::from_str(json_str).unwrap();
if let serde_json::Value::Object(map) = &val {
assert!(
map.contains_key("__proto__"),
"serde_json must decode escape sequences to __proto__"
);
}
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "__proto__");
}
#[test]
fn secret_in_sibling_value_not_in_error_display() {
let secret = "SBSECRETPLANTEDVALUETHATMUSTNEVERAPPEARINERROR12345ABCDEF";
let val = json!({
"account_id": secret,
"toJSON": "irrelevant_value"
});
let err = validate_toolset_tool_args(&val).unwrap_err();
let display = err.to_string();
assert!(
!display.contains(secret),
"error Display must not contain the planted secret: {display}"
);
assert!(
display.contains("toJSON"),
"error Display must mention the matched denylist constant: {display}"
);
}
#[test]
fn error_names_denylist_constant_not_input() {
let val = json!({ "__proto__": "polluted" });
let err = validate_toolset_tool_args(&val).unwrap_err();
if let ToolsetArgsError::DangerousKey { matched_key } = &err {
assert!(
ARGS_KEY_DENYLIST.contains(matched_key),
"matched_key must be a denylist constant, got {matched_key:?}"
);
assert_eq!(*matched_key, "__proto__");
} else {
panic!("expected DangerousKey, got {err:?}");
}
}
#[test]
fn benign_key_before_dangerous_key_still_caught() {
let val = json!({
"account_id": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
"chain_id": "stellar:testnet",
"__proto__": "evil"
});
let r = validate_toolset_tool_args(&val);
assert!(
r.is_err(),
"dangerous key must be caught regardless of position"
);
}
#[test]
fn dangerous_key_in_object_inside_array_inside_object_caught() {
let val = json!({
"records": [
{ "safe": "value" },
{ "constructor": "pollution" }
]
});
let r = validate_toolset_tool_args(&val);
assert_dangerous_key(&r, "constructor");
}
#[test]
fn denylist_has_exactly_11_entries() {
assert_eq!(
ARGS_KEY_DENYLIST.len(),
11,
"ARGS_KEY_DENYLIST must have exactly 11 entries"
);
}
#[test]
fn wide_object_over_node_cap_rejected() {
let mut map = serde_json::Map::new();
for i in 0..=TOOLSET_ARGS_MAX_NODES {
map.insert(format!("field_{i}"), serde_json::Value::String("v".into()));
}
let val = serde_json::Value::Object(map);
let r = validate_toolset_tool_args(&val);
assert!(
matches!(r, Err(ToolsetArgsError::TooManyNodes { .. })),
"expected TooManyNodes for wide-over-cap object, got {r:?}"
);
}
#[test]
fn wide_object_under_node_cap_passes() {
let half = TOOLSET_ARGS_MAX_NODES / 2;
let mut map = serde_json::Map::new();
for i in 0..half {
map.insert(format!("safe_{i}"), serde_json::Value::String("ok".into()));
}
let val = serde_json::Value::Object(map);
validate_toolset_tool_args(&val).unwrap();
}
}