Skip to main content

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}