Skip to main content

pjson_rs/config/
security.rs

1//! Security configuration and limits
2
3use crate::config::ConfigError;
4use crate::security::compression_bomb::CompressionBombConfig;
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8/// Security configuration for the PJS system
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
10pub struct SecurityConfig {
11    /// JSON processing limits
12    pub json: JsonLimits,
13
14    /// Buffer management limits
15    pub buffers: BufferLimits,
16
17    /// Network and connection limits
18    pub network: NetworkLimits,
19
20    /// Session management limits
21    pub sessions: SessionLimits,
22}
23
24/// Hard ceiling on [`JsonLimits::max_depth`], enforced by [`SecurityConfig::validate`].
25///
26/// `sonic-rs` has no internal recursion limit, so `max_depth` is the only
27/// guard between untrusted input and unbounded parser recursion. This caps
28/// misconfiguration (e.g. an operator or deserialized config setting an
29/// unreasonably large limit) from reopening the stack-exhaustion risk fixed
30/// in #456; it does not by itself guarantee safety on every stack size.
31pub const MAX_SAFE_JSON_DEPTH: usize = 512;
32
33/// JSON processing security limits
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct JsonLimits {
36    /// Maximum JSON input size in bytes
37    pub max_input_size: usize,
38
39    /// Maximum JSON nesting depth
40    pub max_depth: usize,
41
42    /// Maximum number of keys in a JSON object
43    pub max_object_keys: usize,
44
45    /// Maximum array length
46    pub max_array_length: usize,
47
48    /// Maximum string length in JSON
49    pub max_string_length: usize,
50}
51
52/// Buffer management security limits
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct BufferLimits {
55    /// Maximum individual buffer size
56    pub max_buffer_size: usize,
57
58    /// Maximum number of buffers in pool
59    pub max_pool_size: usize,
60
61    /// Maximum total memory for all buffer pools
62    pub max_total_memory: usize,
63
64    /// Buffer time-to-live before cleanup
65    pub buffer_ttl_secs: u64,
66
67    /// Maximum buffers per size bucket
68    pub max_buffers_per_bucket: usize,
69}
70
71/// Network and connection security limits
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct NetworkLimits {
74    /// Maximum WebSocket frame size
75    pub max_websocket_frame_size: usize,
76
77    /// Maximum number of concurrent connections
78    pub max_concurrent_connections: usize,
79
80    /// Connection timeout in seconds
81    pub connection_timeout_secs: u64,
82
83    /// Maximum request rate per connection (requests per second)
84    pub max_requests_per_second: u32,
85
86    /// Maximum payload size for HTTP requests
87    pub max_http_payload_size: usize,
88
89    /// Rate limiting configuration
90    pub rate_limiting: RateLimitingConfig,
91
92    /// Compression bomb protection configuration
93    pub compression_bomb: CompressionBombConfig,
94}
95
96/// Rate limiting configuration for DoS protection
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct RateLimitingConfig {
99    /// Maximum requests per time window per IP
100    pub max_requests_per_window: u32,
101
102    /// Time window for rate limiting in seconds
103    pub window_duration_secs: u64,
104
105    /// Maximum concurrent connections per IP
106    pub max_connections_per_ip: usize,
107
108    /// Maximum WebSocket messages per second per connection
109    pub max_messages_per_second: u32,
110
111    /// Burst allowance (extra messages above rate)
112    pub burst_allowance: u32,
113
114    /// Enable rate limiting
115    pub enabled: bool,
116}
117
118/// Session management security limits
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct SessionLimits {
121    /// Maximum session ID length
122    pub max_session_id_length: usize,
123
124    /// Minimum session ID length
125    pub min_session_id_length: usize,
126
127    /// Maximum streams per session
128    pub max_streams_per_session: usize,
129
130    /// Session idle timeout in seconds
131    pub session_timeout_secs: u64,
132
133    /// Maximum session data size
134    pub max_session_data_size: usize,
135}
136
137impl Default for JsonLimits {
138    fn default() -> Self {
139        Self {
140            max_input_size: 100 * 1024 * 1024, // 100MB
141            max_depth: 64,
142            max_object_keys: 10_000,
143            max_array_length: 1_000_000,
144            max_string_length: 10 * 1024 * 1024, // 10MB
145        }
146    }
147}
148
149impl Default for BufferLimits {
150    fn default() -> Self {
151        Self {
152            max_buffer_size: 256 * 1024 * 1024, // 256MB
153            max_pool_size: 1000,
154            max_total_memory: 512 * 1024 * 1024, // 512MB
155            buffer_ttl_secs: 300,                // 5 minutes
156            max_buffers_per_bucket: 50,
157        }
158    }
159}
160
161impl Default for NetworkLimits {
162    fn default() -> Self {
163        Self {
164            max_websocket_frame_size: 16 * 1024 * 1024, // 16MB
165            max_concurrent_connections: 10_000,
166            connection_timeout_secs: 30,
167            max_requests_per_second: 100,
168            max_http_payload_size: 50 * 1024 * 1024, // 50MB
169            rate_limiting: RateLimitingConfig::default(),
170            compression_bomb: CompressionBombConfig::default(),
171        }
172    }
173}
174
175impl Default for RateLimitingConfig {
176    fn default() -> Self {
177        Self {
178            max_requests_per_window: 100,
179            window_duration_secs: 60,
180            max_connections_per_ip: 10,
181            max_messages_per_second: 30,
182            burst_allowance: 5,
183            enabled: true,
184        }
185    }
186}
187
188impl Default for SessionLimits {
189    fn default() -> Self {
190        Self {
191            max_session_id_length: 128,
192            min_session_id_length: 8,
193            max_streams_per_session: 100,
194            session_timeout_secs: 3600,               // 1 hour
195            max_session_data_size: 100 * 1024 * 1024, // 100MB
196        }
197    }
198}
199
200impl SecurityConfig {
201    /// Validate all security configuration values.
202    ///
203    /// Returns `Err` if any invariant is violated (e.g. `min_session_id_length`
204    /// exceeds `max_session_id_length`, or a size limit is zero).
205    ///
206    /// # Errors
207    ///
208    /// Returns [`ConfigError::MustBePositive`] when a size field is zero.
209    /// Returns [`ConfigError::InconsistentBounds`] when min > max for session
210    /// ID length, or `max_depth` exceeds [`MAX_SAFE_JSON_DEPTH`].
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// use pjson_rs::config::SecurityConfig;
216    ///
217    /// SecurityConfig::default().validate().expect("defaults are valid");
218    /// ```
219    pub fn validate(&self) -> Result<(), ConfigError> {
220        let s = "security.sessions";
221
222        if self.sessions.min_session_id_length > self.sessions.max_session_id_length {
223            return Err(ConfigError::InconsistentBounds {
224                section: s,
225                message: "min_session_id_length must be <= max_session_id_length",
226            });
227        }
228
229        if self.json.max_depth > MAX_SAFE_JSON_DEPTH {
230            return Err(ConfigError::InconsistentBounds {
231                section: "security.json",
232                message: "max_depth exceeds MAX_SAFE_JSON_DEPTH (sonic-rs has no internal \
233                           recursion limit; excessive depth risks stack exhaustion)",
234            });
235        }
236
237        macro_rules! must_be_positive {
238            ($section:expr, $field:expr, $value:expr) => {
239                if $value == 0 {
240                    return Err(ConfigError::MustBePositive {
241                        section: $section,
242                        field: $field,
243                    });
244                }
245            };
246        }
247
248        must_be_positive!("security.json", "max_input_size", self.json.max_input_size);
249        must_be_positive!("security.json", "max_depth", self.json.max_depth);
250        must_be_positive!(
251            "security.json",
252            "max_string_length",
253            self.json.max_string_length
254        );
255        must_be_positive!(
256            "security.buffers",
257            "max_buffer_size",
258            self.buffers.max_buffer_size
259        );
260        must_be_positive!(
261            "security.buffers",
262            "max_total_memory",
263            self.buffers.max_total_memory
264        );
265        must_be_positive!(
266            "security.network",
267            "max_websocket_frame_size",
268            self.network.max_websocket_frame_size
269        );
270        must_be_positive!(
271            "security.network",
272            "max_http_payload_size",
273            self.network.max_http_payload_size
274        );
275        must_be_positive!(
276            "security.sessions",
277            "max_session_id_length",
278            self.sessions.max_session_id_length
279        );
280        must_be_positive!(
281            "security.sessions",
282            "min_session_id_length",
283            self.sessions.min_session_id_length
284        );
285        must_be_positive!(
286            "security.sessions",
287            "max_session_data_size",
288            self.sessions.max_session_data_size
289        );
290
291        Ok(())
292    }
293
294    /// Create a configuration optimized for high-throughput scenarios
295    pub fn high_throughput() -> Self {
296        Self {
297            json: JsonLimits {
298                max_input_size: 500 * 1024 * 1024, // 500MB
299                max_depth: 128,
300                max_object_keys: 50_000,
301                max_array_length: 5_000_000,
302                max_string_length: 50 * 1024 * 1024, // 50MB
303            },
304            buffers: BufferLimits {
305                max_buffer_size: 1024 * 1024 * 1024, // 1GB
306                max_pool_size: 5000,
307                max_total_memory: 2 * 1024 * 1024 * 1024, // 2GB
308                buffer_ttl_secs: 600,                     // 10 minutes
309                max_buffers_per_bucket: 200,
310            },
311            network: NetworkLimits {
312                max_websocket_frame_size: 100 * 1024 * 1024, // 100MB
313                max_concurrent_connections: 50_000,
314                connection_timeout_secs: 60,
315                max_requests_per_second: 1000,
316                max_http_payload_size: 200 * 1024 * 1024, // 200MB
317                rate_limiting: RateLimitingConfig {
318                    max_requests_per_window: 1000,
319                    window_duration_secs: 60,
320                    max_connections_per_ip: 50,
321                    max_messages_per_second: 100,
322                    burst_allowance: 20,
323                    enabled: true,
324                },
325                compression_bomb: CompressionBombConfig::high_throughput(),
326            },
327            sessions: SessionLimits {
328                max_session_id_length: 256,
329                min_session_id_length: 16,
330                max_streams_per_session: 1000,
331                session_timeout_secs: 7200,               // 2 hours
332                max_session_data_size: 500 * 1024 * 1024, // 500MB
333            },
334        }
335    }
336
337    /// Create a configuration optimized for low-memory environments
338    pub fn low_memory() -> Self {
339        Self {
340            json: JsonLimits {
341                max_input_size: 10 * 1024 * 1024, // 10MB
342                max_depth: 32,
343                max_object_keys: 1_000,
344                max_array_length: 100_000,
345                max_string_length: 1024 * 1024, // 1MB
346            },
347            buffers: BufferLimits {
348                max_buffer_size: 10 * 1024 * 1024, // 10MB
349                max_pool_size: 100,
350                max_total_memory: 50 * 1024 * 1024, // 50MB
351                buffer_ttl_secs: 60,                // 1 minute
352                max_buffers_per_bucket: 10,
353            },
354            network: NetworkLimits {
355                max_websocket_frame_size: 1024 * 1024, // 1MB
356                max_concurrent_connections: 1_000,
357                connection_timeout_secs: 15,
358                max_requests_per_second: 10,
359                max_http_payload_size: 5 * 1024 * 1024, // 5MB
360                rate_limiting: RateLimitingConfig {
361                    max_requests_per_window: 20,
362                    window_duration_secs: 60,
363                    max_connections_per_ip: 2,
364                    max_messages_per_second: 5,
365                    burst_allowance: 2,
366                    enabled: true,
367                },
368                compression_bomb: CompressionBombConfig::low_memory(),
369            },
370            sessions: SessionLimits {
371                max_session_id_length: 64,
372                min_session_id_length: 8,
373                max_streams_per_session: 10,
374                session_timeout_secs: 900,               // 15 minutes
375                max_session_data_size: 10 * 1024 * 1024, // 10MB
376            },
377        }
378    }
379
380    /// Create a configuration optimized for development/testing
381    pub fn development() -> Self {
382        Self {
383            json: JsonLimits {
384                max_input_size: 50 * 1024 * 1024, // 50MB
385                max_depth: 64,
386                max_object_keys: 5_000,
387                max_array_length: 500_000,
388                max_string_length: 5 * 1024 * 1024, // 5MB
389            },
390            buffers: BufferLimits {
391                max_buffer_size: 100 * 1024 * 1024, // 100MB
392                max_pool_size: 500,
393                max_total_memory: 200 * 1024 * 1024, // 200MB
394                buffer_ttl_secs: 120,                // 2 minutes
395                max_buffers_per_bucket: 25,
396            },
397            network: NetworkLimits {
398                max_websocket_frame_size: 10 * 1024 * 1024, // 10MB
399                max_concurrent_connections: 1_000,
400                connection_timeout_secs: 30,
401                max_requests_per_second: 50,
402                max_http_payload_size: 25 * 1024 * 1024, // 25MB
403                rate_limiting: RateLimitingConfig {
404                    max_requests_per_window: 200,
405                    window_duration_secs: 60,
406                    max_connections_per_ip: 20,
407                    max_messages_per_second: 50,
408                    burst_allowance: 10,
409                    enabled: true,
410                },
411                compression_bomb: CompressionBombConfig::default(),
412            },
413            sessions: SessionLimits {
414                max_session_id_length: 128,
415                min_session_id_length: 8,
416                max_streams_per_session: 50,
417                session_timeout_secs: 1800,              // 30 minutes
418                max_session_data_size: 50 * 1024 * 1024, // 50MB
419            },
420        }
421    }
422
423    /// Get buffer TTL as Duration
424    pub fn buffer_ttl(&self) -> Duration {
425        Duration::from_secs(self.buffers.buffer_ttl_secs)
426    }
427
428    /// Get connection timeout as Duration
429    pub fn connection_timeout(&self) -> Duration {
430        Duration::from_secs(self.network.connection_timeout_secs)
431    }
432
433    /// Get session timeout as Duration
434    pub fn session_timeout(&self) -> Duration {
435        Duration::from_secs(self.sessions.session_timeout_secs)
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442    use crate::config::ConfigError;
443
444    #[test]
445    fn test_default_security_config() {
446        let config = SecurityConfig::default();
447
448        // Test reasonable defaults
449        assert!(config.json.max_input_size > 0);
450        assert!(config.buffers.max_buffer_size > 0);
451        assert!(config.network.max_concurrent_connections > 0);
452        assert!(config.sessions.max_session_id_length >= config.sessions.min_session_id_length);
453    }
454
455    #[test]
456    fn test_security_config_default_validates() {
457        SecurityConfig::default()
458            .validate()
459            .expect("SecurityConfig::default() must be valid");
460    }
461
462    #[test]
463    fn test_rejects_min_session_id_length_greater_than_max() {
464        let mut config = SecurityConfig::default();
465        config.sessions.min_session_id_length = 200;
466        config.sessions.max_session_id_length = 100;
467        let err = config.validate().unwrap_err();
468        assert!(matches!(
469            err,
470            ConfigError::InconsistentBounds {
471                section: "security.sessions",
472                ..
473            }
474        ));
475    }
476
477    #[test]
478    fn test_rejects_max_depth_above_hard_ceiling() {
479        let mut config = SecurityConfig::default();
480        config.json.max_depth = MAX_SAFE_JSON_DEPTH + 1;
481        let err = config.validate().unwrap_err();
482        assert!(matches!(
483            err,
484            ConfigError::InconsistentBounds {
485                section: "security.json",
486                ..
487            }
488        ));
489    }
490
491    #[test]
492    fn test_max_depth_exactly_at_hard_ceiling_validates() {
493        let mut config = SecurityConfig::default();
494        config.json.max_depth = MAX_SAFE_JSON_DEPTH;
495        config.validate().expect("boundary value must be valid");
496    }
497
498    #[test]
499    fn test_max_deserialize_depth_matches_domain_guard_defaults() {
500        // Message only claims the *default* coupling: `high_throughput()` (128)
501        // and `low_memory()` (32) intentionally diverge from
502        // `MAX_DESERIALIZE_DEPTH` and are not, and should not be, covered here.
503        // If this fails, lower the higher value to match — do not raise the
504        // lower one, since both bound stack depth per #464.
505        let msg = "pjs-domain's MAX_DESERIALIZE_DEPTH (a hard ceiling enforced by \
506                    JsonData's Deserialize impl) and pjs-core's JsonLimits::max_depth \
507                    (a configurable knob enforced by SecurityValidator) should default \
508                    to the same value, so a default JsonLimits does not admit input \
509                    that JsonData::deserialize would then reject";
510
511        assert_eq!(
512            pjson_rs_domain::MAX_DESERIALIZE_DEPTH,
513            SecurityConfig::default().json.max_depth,
514            "{msg}"
515        );
516        assert_eq!(
517            pjson_rs_domain::MAX_DESERIALIZE_DEPTH,
518            SecurityConfig::development().json.max_depth,
519            "{msg}"
520        );
521    }
522
523    #[cfg(feature = "partial-parse")]
524    #[test]
525    fn test_jiter_config_default_max_depth_matches_domain_guard() {
526        assert_eq!(
527            pjson_rs_domain::MAX_DESERIALIZE_DEPTH,
528            crate::parser::partial::JiterConfig::default().max_depth,
529            "JiterPartialParser's default max_depth should match pjs-domain's \
530             MAX_DESERIALIZE_DEPTH hard ceiling"
531        );
532    }
533
534    #[test]
535    fn test_high_throughput_config() {
536        let config = SecurityConfig::high_throughput();
537        let default = SecurityConfig::default();
538
539        // High throughput should have higher limits
540        assert!(config.json.max_input_size >= default.json.max_input_size);
541        assert!(config.buffers.max_total_memory >= default.buffers.max_total_memory);
542        assert!(
543            config.network.max_concurrent_connections >= default.network.max_concurrent_connections
544        );
545    }
546
547    #[test]
548    fn test_low_memory_config() {
549        let config = SecurityConfig::low_memory();
550        let default = SecurityConfig::default();
551
552        // Low memory should have lower limits
553        assert!(config.json.max_input_size <= default.json.max_input_size);
554        assert!(config.buffers.max_total_memory <= default.buffers.max_total_memory);
555        assert!(config.buffers.max_buffers_per_bucket <= default.buffers.max_buffers_per_bucket);
556    }
557
558    #[test]
559    fn test_duration_conversions() {
560        let config = SecurityConfig::default();
561
562        assert!(config.buffer_ttl().as_secs() > 0);
563        assert!(config.connection_timeout().as_secs() > 0);
564        assert!(config.session_timeout().as_secs() > 0);
565    }
566}