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                    map.remove("__proto__");
98                }
99
100                if options.constructor_action != Action::Ignore && is_constructor_violation(map) {
101                    if options.safe {
102                        return Walk::Null;
103                    }
104                    if options.constructor_action == Action::Error {
105                        return Walk::Error;
106                    }
107                    map.remove("constructor");
108                }
109
110                worklist.extend(map.values_mut());
111            }
112            Value::Array(items) => worklist.extend(items.iter_mut()),
113            // Scalars and null hold no scannable children.
114            _ => {}
115        }
116    }
117    Walk::Clean
118}
119
120/// Test whether a map has a `constructor` key that nests a `prototype` key.
121///
122/// A violation needs all of: an own `constructor` key, whose value is a JSON
123/// object, that object holds a `prototype` key. A `null`, array, string,
124/// number, or boolean value for `constructor` is not a violation, and neither
125/// is an object without a `prototype` child.
126fn is_constructor_violation(map: &serde_json::Map<String, Value>) -> bool {
127    match map.get("constructor") {
128        Some(Value::Object(inner)) => inner.contains_key("prototype"),
129        _ => false,
130    }
131}