secure_json_parse/scan.rs
1//! The tree walk that detects, removes, or rejects forbidden keys.
2
3use crate::{Action, Error, Options};
4use serde_json::Value;
5
6/// Walk a parsed value and apply the prototype-poisoning checks.
7///
8/// `value` is consumed and the cleaned value is returned on success. Under
9/// [`Action::Remove`] the forbidden keys are dropped from the returned value.
10/// Scalars and `null` are returned unchanged. Objects and arrays are walked.
11///
12/// Returns `Ok(Some(value))` when the value is clean or only had keys removed.
13/// Returns `Ok(None)` when `safe` is on and a violation is found. Returns
14/// [`Error::ForbiddenProperty`] when a violation is found, the matching
15/// [`Action`] is [`Action::Error`], and `safe` is off.
16///
17/// Use this when you already hold a [`Value`] and want the same checks without
18/// re-parsing. It does no JSON parsing.
19///
20/// # Errors
21///
22/// Returns [`Error::ForbiddenProperty`] on a violation under [`Action::Error`]
23/// with `safe` off.
24///
25/// # Examples
26///
27/// ```
28/// use secure_json_parse::{scan, Action, Options};
29/// use serde_json::json;
30///
31/// let opts = Options::default().proto_action(Action::Remove);
32/// let cleaned = scan(json!({"a": 1, "__proto__": {"x": 2}}), &opts)
33/// .unwrap()
34/// .unwrap();
35/// assert_eq!(cleaned, json!({"a": 1}));
36/// ```
37pub fn scan(mut value: Value, options: &Options) -> Result<Option<Value>, Error> {
38 if options.proto_action == Action::Ignore && options.constructor_action == Action::Ignore {
39 return Ok(Some(value));
40 }
41 match walk(&mut value, options) {
42 Walk::Clean => Ok(Some(value)),
43 // A rejection drops `value` here. The walk may stop early, so an unwalked
44 // deep sibling can still be present. Drain it so the drop cannot overflow.
45 Walk::Null => {
46 drain(value);
47 Ok(None)
48 }
49 Walk::Error => {
50 drain(value);
51 Err(Error::ForbiddenProperty)
52 }
53 }
54}
55
56/// Drop a value without recursing, so a deep tree cannot overflow the stack.
57fn drain(value: Value) {
58 let mut stack = vec![value];
59 while let Some(node) = stack.pop() {
60 match node {
61 Value::Array(items) => stack.extend(items),
62 Value::Object(map) => stack.extend(map.into_iter().map(|(_, v)| v)),
63 _ => {}
64 }
65 }
66}
67
68/// Result of walking one subtree.
69enum Walk {
70 /// No violation, or violations removed.
71 Clean,
72 /// A violation under `safe` mode. Maps to `Ok(None)`.
73 Null,
74 /// A violation under [`Action::Error`]. Maps to [`Error::ForbiddenProperty`].
75 Error,
76}
77
78/// Check and clean every node in the tree.
79///
80/// Within a node the `__proto__` check runs first, then the `constructor`
81/// check, then children are visited. Under [`Action::Remove`] a removed subtree
82/// is dropped before its children are enqueued, so it is never visited again.
83/// The walk uses an explicit worklist, so stack use stays flat no matter how
84/// deep the tree is.
85fn walk(root: &mut Value, options: &Options) -> Walk {
86 let mut worklist: Vec<&mut Value> = vec![root];
87 while let Some(node) = worklist.pop() {
88 match node {
89 Value::Object(map) => {
90 if options.proto_action != Action::Ignore && map.contains_key("__proto__") {
91 if options.safe {
92 return Walk::Null;
93 }
94 if options.proto_action == Action::Error {
95 return Walk::Error;
96 }
97 if let Some(removed) = map.remove("__proto__") {
98 drain(removed);
99 }
100 }
101
102 if options.constructor_action != Action::Ignore && is_constructor_violation(map) {
103 if options.safe {
104 return Walk::Null;
105 }
106 if options.constructor_action == Action::Error {
107 return Walk::Error;
108 }
109 if let Some(removed) = map.remove("constructor") {
110 drain(removed);
111 }
112 }
113
114 worklist.extend(map.values_mut());
115 }
116 Value::Array(items) => worklist.extend(items.iter_mut()),
117 // Scalars and null hold no scannable children.
118 _ => {}
119 }
120 }
121 Walk::Clean
122}
123
124/// Test whether a map has a `constructor` key that nests a `prototype` key.
125///
126/// A violation needs all of: an own `constructor` key, whose value is a JSON
127/// object, that object holds a `prototype` key. A `null`, array, string,
128/// number, or boolean value for `constructor` is not a violation, and neither
129/// is an object without a `prototype` child.
130fn is_constructor_violation(map: &serde_json::Map<String, Value>) -> bool {
131 match map.get("constructor") {
132 Some(Value::Object(inner)) => inner.contains_key("prototype"),
133 _ => false,
134 }
135}