Skip to main content

secure_json_parse/
lib.rs

1//! Parse JSON while blocking prototype-poisoning keys.
2//!
3//! This crate parses untrusted JSON and detects two key patterns that pollute a
4//! JavaScript object's prototype once the parsed value is copied, merged, or
5//! iterated:
6//!
7//! 1. a key literally named `__proto__`
8//! 2. a key named `constructor` whose value is an object that contains a
9//!    `prototype` key (a `constructor.prototype` nesting)
10//!
11//! For each pattern you choose an [`Action`]: error out, remove the key, or
12//! ignore it and behave like a plain JSON parse. A `safe` flag turns any
13//! detected violation into `Ok(None)` instead of an error.
14//!
15//! The parsed value is a [`serde_json::Value`]. In a `Value` tree `__proto__`
16//! and `constructor` are ordinary map keys, so the danger is latent rather than
17//! immediate. The job here is to detect, remove, or reject those keys before the
18//! value reaches code that would treat them as a prototype.
19//!
20//! # Quick start
21//!
22//! ```
23//! use secure_json_parse::{parse, Action, Options, Error};
24//!
25//! // Default options error on a forbidden key.
26//! let err = parse(r#"{"__proto__": {"x": 7}}"#, &Options::default());
27//! assert!(matches!(err, Err(Error::ForbiddenProperty)));
28//!
29//! // Remove the key instead.
30//! let opts = Options::default().proto_action(Action::Remove);
31//! let value = parse(r#"{"a": 5, "__proto__": {"x": 7}}"#, &opts).unwrap().unwrap();
32//! assert_eq!(value, serde_json::json!({"a": 5}));
33//! ```
34//!
35//! # Safe parsing
36//!
37//! [`safe_parse`] folds every outcome into one three-valued result:
38//!
39//! ```
40//! use secure_json_parse::{safe_parse, SafeOutcome};
41//!
42//! // Clean input parses to a value.
43//! assert!(matches!(safe_parse(r#"{"a": 1}"#), SafeOutcome::Value(_)));
44//! // A forbidden key yields Violation.
45//! assert!(matches!(safe_parse(r#"{"__proto__": {}}"#), SafeOutcome::Violation));
46//! // Malformed JSON yields Malformed.
47//! assert!(matches!(safe_parse(r#"{"a": "#), SafeOutcome::Malformed));
48//! ```
49
50#![forbid(unsafe_code)]
51#![warn(missing_docs)]
52
53mod scan;
54
55pub use scan::scan;
56
57use serde_json::Value;
58
59/// What to do when a forbidden key is found.
60///
61/// `proto_action` and `constructor_action` each take one of these. The default
62/// is [`Action::Error`].
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
64pub enum Action {
65    /// Reject the input with [`Error::ForbiddenProperty`].
66    #[default]
67    Error,
68    /// Delete the offending key from the result.
69    Remove,
70    /// Skip the check for this key and keep it.
71    Ignore,
72}
73
74/// Configuration for [`parse`], [`parse_bytes`], and [`scan`].
75///
76/// Build one with [`Options::default`] and the chained setters. The defaults
77/// match a strict parser: both actions are [`Action::Error`] and `safe` is off.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79pub struct Options {
80    /// Action for a `__proto__` key.
81    pub proto_action: Action,
82    /// Action for a `constructor.prototype` nesting.
83    pub constructor_action: Action,
84    /// When true, a detected violation returns `Ok(None)` instead of an error.
85    pub safe: bool,
86}
87
88impl Default for Options {
89    fn default() -> Self {
90        Options {
91            proto_action: Action::Error,
92            constructor_action: Action::Error,
93            safe: false,
94        }
95    }
96}
97
98impl Options {
99    /// Set the action for `__proto__` keys.
100    #[must_use]
101    pub fn proto_action(mut self, action: Action) -> Self {
102        self.proto_action = action;
103        self
104    }
105
106    /// Set the action for `constructor.prototype` nestings.
107    #[must_use]
108    pub fn constructor_action(mut self, action: Action) -> Self {
109        self.constructor_action = action;
110        self
111    }
112
113    /// Set the `safe` flag. When true a violation returns `Ok(None)`.
114    #[must_use]
115    pub fn safe(mut self, safe: bool) -> Self {
116        self.safe = safe;
117        self
118    }
119}
120
121/// Why a parse failed.
122///
123/// [`Error::Syntax`] wraps a malformed-JSON error from the underlying parser.
124/// [`Error::ForbiddenProperty`] reports a `__proto__` or `constructor.prototype`
125/// violation when the relevant [`Action`] is [`Action::Error`] and `safe` is
126/// off. Both violation kinds share one variant and one message.
127#[derive(Debug)]
128pub enum Error {
129    /// The JSON text was malformed.
130    Syntax(serde_json::Error),
131    /// The value held a forbidden prototype property.
132    ForbiddenProperty,
133}
134
135impl std::fmt::Display for Error {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        match self {
138            Error::Syntax(e) => write!(f, "{e}"),
139            Error::ForbiddenProperty => write!(f, "Object contains forbidden prototype property"),
140        }
141    }
142}
143
144impl std::error::Error for Error {
145    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
146        match self {
147            Error::Syntax(e) => Some(e),
148            Error::ForbiddenProperty => None,
149        }
150    }
151}
152
153/// The three outcomes of [`safe_parse`].
154///
155/// The variant names state the outcome. The JavaScript `safeParse` maps the
156/// same three cases to a value, `null` for a violation, and `undefined` for a
157/// parse error.
158#[derive(Debug)]
159pub enum SafeOutcome {
160    /// Parsed cleanly. Holds the value.
161    Value(Value),
162    /// A forbidden prototype property was found. JavaScript returns `null`.
163    Violation,
164    /// The JSON text was malformed. JavaScript returns `undefined`.
165    Malformed,
166}
167
168/// Drop a leading `U+FEFF` byte order mark from a string.
169///
170/// The UTF-8 BOM is `EF BB BF`, which decodes to `U+FEFF`. Removing it before
171/// parsing matches a JavaScript parser that strips a leading BOM.
172fn strip_bom(text: &str) -> &str {
173    text.strip_prefix('\u{FEFF}').unwrap_or(text)
174}
175
176/// Parse JSON text and apply prototype-poisoning checks.
177///
178/// On success returns `Ok(Some(value))`. When `safe` is on and a violation is
179/// found, returns `Ok(None)`. A malformed input gives [`Error::Syntax`]; a
180/// violation under [`Action::Error`] gives [`Error::ForbiddenProperty`].
181///
182/// Scalars and `null` skip scanning and are returned as is. Objects and arrays
183/// are walked.
184///
185/// # Errors
186///
187/// Returns [`Error::Syntax`] if `text` is not valid JSON, or
188/// [`Error::ForbiddenProperty`] if a forbidden key is found and the matching
189/// [`Action`] is [`Action::Error`] with `safe` off.
190///
191/// # Examples
192///
193/// ```
194/// use secure_json_parse::{parse, Action, Options};
195///
196/// let opts = Options::default().constructor_action(Action::Remove);
197/// let v = parse(r#"{"constructor": {"prototype": {}}, "a": 1}"#, &opts)
198///     .unwrap()
199///     .unwrap();
200/// assert_eq!(v, serde_json::json!({"a": 1}));
201/// ```
202pub fn parse(text: &str, options: &Options) -> Result<Option<Value>, Error> {
203    let text = strip_bom(text);
204    let value: Value = serde_json::from_str(text).map_err(Error::Syntax)?;
205    scan(value, options)
206}
207
208/// Parse JSON bytes and apply prototype-poisoning checks.
209///
210/// Same as [`parse`] but takes UTF-8 bytes. A leading UTF-8 byte order mark
211/// (`EF BB BF`) is stripped before parsing.
212///
213/// # Errors
214///
215/// Returns [`Error::Syntax`] if the bytes are not valid UTF-8 JSON, or
216/// [`Error::ForbiddenProperty`] under the same rules as [`parse`].
217pub fn parse_bytes(bytes: &[u8], options: &Options) -> Result<Option<Value>, Error> {
218    let bytes = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(bytes);
219    let value: Value = serde_json::from_slice(bytes).map_err(Error::Syntax)?;
220    scan(value, options)
221}
222
223/// Parse JSON text and fold every outcome into a [`SafeOutcome`].
224///
225/// Runs with both actions at [`Action::Error`] and `safe` on. A clean parse
226/// returns [`SafeOutcome::Value`]. A forbidden key returns
227/// [`SafeOutcome::Violation`]. Malformed JSON returns [`SafeOutcome::Malformed`].
228/// This never returns an error type.
229///
230/// # Examples
231///
232/// ```
233/// use secure_json_parse::{safe_parse, SafeOutcome};
234///
235/// match safe_parse(r#"{"a": 1}"#) {
236///     SafeOutcome::Value(v) => assert_eq!(v, serde_json::json!({"a": 1})),
237///     _ => panic!("expected a value"),
238/// }
239/// ```
240pub fn safe_parse(text: &str) -> SafeOutcome {
241    let opts = Options::default().safe(true);
242    fold(parse(text, &opts))
243}
244
245/// Parse JSON bytes and fold every outcome into a [`SafeOutcome`].
246///
247/// The byte counterpart to [`safe_parse`]. A leading UTF-8 byte order mark is
248/// stripped before parsing.
249pub fn safe_parse_bytes(bytes: &[u8]) -> SafeOutcome {
250    let opts = Options::default().safe(true);
251    fold(parse_bytes(bytes, &opts))
252}
253
254/// Collapse a parse result into a [`SafeOutcome`].
255fn fold(result: Result<Option<Value>, Error>) -> SafeOutcome {
256    match result {
257        Ok(Some(value)) => SafeOutcome::Value(value),
258        Ok(None) => SafeOutcome::Violation,
259        Err(_) => SafeOutcome::Malformed,
260    }
261}