secure-json-parse 0.1.1

prototype-poisoning-safe JSON parsing of untrusted input
Documentation
//! The tree walk that detects, removes, or rejects forbidden keys.

use crate::{Action, Error, Options};
use serde_json::Value;

/// Walk a parsed value and apply the prototype-poisoning checks.
///
/// `value` is consumed and the cleaned value is returned on success. Under
/// [`Action::Remove`] the forbidden keys are dropped from the returned value.
/// Scalars and `null` are returned unchanged. Objects and arrays are walked.
///
/// Returns `Ok(Some(value))` when the value is clean or only had keys removed.
/// Returns `Ok(None)` when `safe` is on and a violation is found. Returns
/// [`Error::ForbiddenProperty`] when a violation is found, the matching
/// [`Action`] is [`Action::Error`], and `safe` is off.
///
/// Use this when you already hold a [`Value`] and want the same checks without
/// re-parsing. It does no JSON parsing.
///
/// # Errors
///
/// Returns [`Error::ForbiddenProperty`] on a violation under [`Action::Error`]
/// with `safe` off.
///
/// # Examples
///
/// ```
/// use secure_json_parse::{scan, Action, Options};
/// use serde_json::json;
///
/// let opts = Options::default().proto_action(Action::Remove);
/// let cleaned = scan(json!({"a": 1, "__proto__": {"x": 2}}), &opts)
///     .unwrap()
///     .unwrap();
/// assert_eq!(cleaned, json!({"a": 1}));
/// ```
pub fn scan(mut value: Value, options: &Options) -> Result<Option<Value>, Error> {
    if options.proto_action == Action::Ignore && options.constructor_action == Action::Ignore {
        return Ok(Some(value));
    }
    match walk(&mut value, options) {
        Walk::Clean => Ok(Some(value)),
        // A rejection drops `value` here. The walk may stop early, so an unwalked
        // deep sibling can still be present. Drain it so the drop cannot overflow.
        Walk::Null => {
            drain(value);
            Ok(None)
        }
        Walk::Error => {
            drain(value);
            Err(Error::ForbiddenProperty)
        }
    }
}

/// Drop a value without recursing, so a deep tree cannot overflow the stack.
fn drain(value: Value) {
    let mut stack = vec![value];
    while let Some(node) = stack.pop() {
        match node {
            Value::Array(items) => stack.extend(items),
            Value::Object(map) => stack.extend(map.into_iter().map(|(_, v)| v)),
            _ => {}
        }
    }
}

/// Result of walking one subtree.
enum Walk {
    /// No violation, or violations removed.
    Clean,
    /// A violation under `safe` mode. Maps to `Ok(None)`.
    Null,
    /// A violation under [`Action::Error`]. Maps to [`Error::ForbiddenProperty`].
    Error,
}

/// Check and clean every node in the tree.
///
/// Within a node the `__proto__` check runs first, then the `constructor`
/// check, then children are visited. Under [`Action::Remove`] a removed subtree
/// is dropped before its children are enqueued, so it is never visited again.
/// The walk uses an explicit worklist, so stack use stays flat no matter how
/// deep the tree is.
fn walk(root: &mut Value, options: &Options) -> Walk {
    let mut worklist: Vec<&mut Value> = vec![root];
    while let Some(node) = worklist.pop() {
        match node {
            Value::Object(map) => {
                if options.proto_action != Action::Ignore && map.contains_key("__proto__") {
                    if options.safe {
                        return Walk::Null;
                    }
                    if options.proto_action == Action::Error {
                        return Walk::Error;
                    }
                    if let Some(removed) = map.remove("__proto__") {
                        drain(removed);
                    }
                }

                if options.constructor_action != Action::Ignore && is_constructor_violation(map) {
                    if options.safe {
                        return Walk::Null;
                    }
                    if options.constructor_action == Action::Error {
                        return Walk::Error;
                    }
                    if let Some(removed) = map.remove("constructor") {
                        drain(removed);
                    }
                }

                worklist.extend(map.values_mut());
            }
            Value::Array(items) => worklist.extend(items.iter_mut()),
            // Scalars and null hold no scannable children.
            _ => {}
        }
    }
    Walk::Clean
}

/// Test whether a map has a `constructor` key that nests a `prototype` key.
///
/// A violation needs all of: an own `constructor` key, whose value is a JSON
/// object, that object holds a `prototype` key. A `null`, array, string,
/// number, or boolean value for `constructor` is not a violation, and neither
/// is an object without a `prototype` child.
fn is_constructor_violation(map: &serde_json::Map<String, Value>) -> bool {
    match map.get("constructor") {
        Some(Value::Object(inner)) => inner.contains_key("prototype"),
        _ => false,
    }
}