Skip to main content

xdoc/security/
mod.rs

1//! Security limits and safe defaults shared by higher-level modules.
2//!
3//! Defaults are intentionally conservative for an in-memory XML engine:
4//! external entities, network access, and filesystem access are disabled unless
5//! a future caller opts into a different policy explicitly.
6
7use crate::core::{ErrorKind, XmlError, XmlResult};
8
9pub const DEFAULT_MAX_DOCUMENT_BYTES: usize = 10 * 1024 * 1024;
10pub const DEFAULT_MAX_TEXT_BYTES: usize = 1024 * 1024;
11pub const DEFAULT_MAX_DEPTH: usize = 128;
12pub const DEFAULT_MAX_NODES: usize = 100_000;
13pub const DEFAULT_MAX_QUERY_STEPS: usize = 100_000;
14pub const DEFAULT_MAX_TRANSFORM_EXPANSION: usize = 100_000;
15
16/// Shared resource limits for parser, query, transform, and signature modules.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct SecurityLimits {
19    max_document_bytes: usize,
20    max_text_bytes: usize,
21    max_depth: usize,
22    max_nodes: usize,
23    max_query_steps: usize,
24    max_transform_expansion: usize,
25}
26
27impl SecurityLimits {
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    pub fn with_max_document_bytes(mut self, limit: usize) -> Self {
33        self.max_document_bytes = limit;
34        self
35    }
36
37    pub fn with_max_text_bytes(mut self, limit: usize) -> Self {
38        self.max_text_bytes = limit;
39        self
40    }
41
42    pub fn with_max_depth(mut self, limit: usize) -> Self {
43        self.max_depth = limit;
44        self
45    }
46
47    pub fn with_max_nodes(mut self, limit: usize) -> Self {
48        self.max_nodes = limit;
49        self
50    }
51
52    pub fn with_max_query_steps(mut self, limit: usize) -> Self {
53        self.max_query_steps = limit;
54        self
55    }
56
57    pub fn with_max_transform_expansion(mut self, limit: usize) -> Self {
58        self.max_transform_expansion = limit;
59        self
60    }
61
62    pub fn max_document_bytes(&self) -> usize {
63        self.max_document_bytes
64    }
65
66    pub fn max_text_bytes(&self) -> usize {
67        self.max_text_bytes
68    }
69
70    pub fn max_depth(&self) -> usize {
71        self.max_depth
72    }
73
74    pub fn max_nodes(&self) -> usize {
75        self.max_nodes
76    }
77
78    pub fn max_query_steps(&self) -> usize {
79        self.max_query_steps
80    }
81
82    pub fn max_transform_expansion(&self) -> usize {
83        self.max_transform_expansion
84    }
85
86    pub fn check_document_size(&self, bytes: usize) -> XmlResult<()> {
87        if bytes > self.max_document_bytes {
88            return Err(limit_error(format!(
89                "XML document exceeds maximum size of {} bytes",
90                self.max_document_bytes
91            )));
92        }
93        Ok(())
94    }
95
96    pub fn check_text_size(&self, bytes: usize) -> XmlResult<()> {
97        if bytes > self.max_text_bytes {
98            return Err(limit_error(format!(
99                "XML text exceeds maximum of {} bytes",
100                self.max_text_bytes
101            )));
102        }
103        Ok(())
104    }
105
106    pub fn check_depth(&self, depth: usize) -> XmlResult<()> {
107        if depth > self.max_depth {
108            return Err(limit_error(format!(
109                "XML depth exceeds maximum of {}",
110                self.max_depth
111            )));
112        }
113        Ok(())
114    }
115
116    pub fn check_nodes(&self, nodes: usize) -> XmlResult<()> {
117        if nodes > self.max_nodes {
118            return Err(limit_error(format!(
119                "XML node count exceeds maximum of {}",
120                self.max_nodes
121            )));
122        }
123        Ok(())
124    }
125
126    pub fn check_query_steps(&self, steps: usize) -> XmlResult<()> {
127        if steps > self.max_query_steps {
128            return Err(limit_error(format!(
129                "query step count exceeds maximum of {}",
130                self.max_query_steps
131            )));
132        }
133        Ok(())
134    }
135
136    pub fn check_transform_expansion(&self, expansions: usize) -> XmlResult<()> {
137        if expansions > self.max_transform_expansion {
138            return Err(limit_error(format!(
139                "transform expansion exceeds maximum of {}",
140                self.max_transform_expansion
141            )));
142        }
143        Ok(())
144    }
145}
146
147impl Default for SecurityLimits {
148    fn default() -> Self {
149        Self {
150            max_document_bytes: DEFAULT_MAX_DOCUMENT_BYTES,
151            max_text_bytes: DEFAULT_MAX_TEXT_BYTES,
152            max_depth: DEFAULT_MAX_DEPTH,
153            max_nodes: DEFAULT_MAX_NODES,
154            max_query_steps: DEFAULT_MAX_QUERY_STEPS,
155            max_transform_expansion: DEFAULT_MAX_TRANSFORM_EXPANSION,
156        }
157    }
158}
159
160/// Controls entity and external resource handling.
161#[derive(Debug, Clone, PartialEq, Eq, Default)]
162pub struct EntityPolicy {
163    allow_external_entities: bool,
164    allow_network: bool,
165    allow_filesystem: bool,
166}
167
168impl EntityPolicy {
169    pub fn secure() -> Self {
170        Self::default()
171    }
172
173    pub fn with_external_entities(mut self, allow: bool) -> Self {
174        self.allow_external_entities = allow;
175        self
176    }
177
178    pub fn with_network(mut self, allow: bool) -> Self {
179        self.allow_network = allow;
180        self
181    }
182
183    pub fn with_filesystem(mut self, allow: bool) -> Self {
184        self.allow_filesystem = allow;
185        self
186    }
187
188    pub fn external_entities_allowed(&self) -> bool {
189        self.allow_external_entities
190    }
191
192    pub fn network_allowed(&self) -> bool {
193        self.allow_network
194    }
195
196    pub fn filesystem_allowed(&self) -> bool {
197        self.allow_filesystem
198    }
199
200    pub fn reject_external_entity(&self, entity: &str) -> XmlResult<()> {
201        if self.allow_external_entities {
202            return Ok(());
203        }
204        Err(XmlError::new(
205            ErrorKind::Parse,
206            format!("entity reference `&{entity};` is disabled by default"),
207        ))
208    }
209
210    pub fn reject_doctype(&self) -> XmlResult<()> {
211        if self.allow_external_entities {
212            return Ok(());
213        }
214        Err(XmlError::new(
215            ErrorKind::Parse,
216            "DOCTYPE declarations are disabled by default",
217        ))
218    }
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Default)]
222pub struct ParserSecurityConfig {
223    limits: SecurityLimits,
224    entity_policy: EntityPolicy,
225}
226
227impl ParserSecurityConfig {
228    pub fn new() -> Self {
229        Self::default()
230    }
231
232    pub fn with_limits(mut self, limits: SecurityLimits) -> Self {
233        self.limits = limits;
234        self
235    }
236
237    pub fn with_entity_policy(mut self, policy: EntityPolicy) -> Self {
238        self.entity_policy = policy;
239        self
240    }
241
242    pub fn limits(&self) -> &SecurityLimits {
243        &self.limits
244    }
245
246    pub fn entity_policy(&self) -> &EntityPolicy {
247        &self.entity_policy
248    }
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Default)]
252pub struct QuerySecurityConfig {
253    limits: SecurityLimits,
254}
255
256impl QuerySecurityConfig {
257    pub fn new() -> Self {
258        Self::default()
259    }
260
261    pub fn with_limits(mut self, limits: SecurityLimits) -> Self {
262        self.limits = limits;
263        self
264    }
265
266    pub fn limits(&self) -> &SecurityLimits {
267        &self.limits
268    }
269
270    pub fn check_steps(&self, steps: usize) -> XmlResult<()> {
271        self.limits.check_query_steps(steps)
272    }
273}
274
275#[derive(Debug, Clone, PartialEq, Eq, Default)]
276pub struct TransformSecurityConfig {
277    limits: SecurityLimits,
278}
279
280impl TransformSecurityConfig {
281    pub fn new() -> Self {
282        Self::default()
283    }
284
285    pub fn with_limits(mut self, limits: SecurityLimits) -> Self {
286        self.limits = limits;
287        self
288    }
289
290    pub fn limits(&self) -> &SecurityLimits {
291        &self.limits
292    }
293
294    pub fn check_expansion(&self, expansions: usize) -> XmlResult<()> {
295        self.limits.check_transform_expansion(expansions)
296    }
297}
298
299fn limit_error(message: String) -> XmlError {
300    XmlError::new(ErrorKind::Parse, message)
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn security_defaults_are_safe() {
309        let parser = ParserSecurityConfig::default();
310
311        assert!(!parser.entity_policy().external_entities_allowed());
312        assert!(!parser.entity_policy().network_allowed());
313        assert!(!parser.entity_policy().filesystem_allowed());
314        assert_eq!(parser.limits().max_depth(), DEFAULT_MAX_DEPTH);
315        assert_eq!(
316            parser.limits().max_document_bytes(),
317            DEFAULT_MAX_DOCUMENT_BYTES
318        );
319    }
320
321    #[test]
322    fn security_limits_reject_depth_size_and_nodes() {
323        let limits = SecurityLimits::default()
324            .with_max_depth(1)
325            .with_max_document_bytes(4)
326            .with_max_text_bytes(2)
327            .with_max_nodes(1);
328
329        assert!(limits.check_depth(2).is_err());
330        assert!(limits.check_document_size(5).is_err());
331        assert!(limits.check_text_size(3).is_err());
332        assert!(limits.check_nodes(2).is_err());
333    }
334
335    #[test]
336    fn security_entity_policy_blocks_external_entities_by_default() {
337        let policy = EntityPolicy::default();
338
339        assert_eq!(
340            policy.reject_external_entity("xxe").unwrap_err().kind(),
341            &ErrorKind::Parse
342        );
343        assert_eq!(
344            policy.reject_doctype().unwrap_err().kind(),
345            &ErrorKind::Parse
346        );
347    }
348
349    #[test]
350    fn security_query_has_step_limit() {
351        let config = QuerySecurityConfig::default()
352            .with_limits(SecurityLimits::default().with_max_query_steps(1));
353
354        assert!(config.check_steps(2).is_err());
355    }
356
357    #[test]
358    fn security_transform_rejects_expansion_excess() {
359        let config = TransformSecurityConfig::default()
360            .with_limits(SecurityLimits::default().with_max_transform_expansion(1));
361
362        assert!(config.check_expansion(2).is_err());
363    }
364}