this repo has no description
1use std::sync::Arc;
2use std::time::Duration;
3use tokio::time::sleep;
4use serde_json::{json, Value};
5use reqwest::Client;
6use gigabrain::Graph;
7use gigabrain::server::{ServerConfig, rest::RestServer};
8
9/// Security testing suite for attack surface validation
10/// This module tests the system's resilience against various security threats
11/// and validates that proper security measures are in place.
12
13#[derive(Clone)]
14pub struct SecurityTestClient {
15 client: Client,
16 base_url: String,
17}
18
19impl SecurityTestClient {
20 pub fn new(base_url: &str) -> Self {
21 Self {
22 client: Client::builder()
23 .timeout(Duration::from_secs(10))
24 .build()
25 .expect("Failed to create client"),
26 base_url: base_url.to_string(),
27 }
28 }
29
30 pub async fn post_json(&self, path: &str, body: Value) -> Result<reqwest::Response, reqwest::Error> {
31 self.client
32 .post(&format!("{}{}", self.base_url, path))
33 .header("Content-Type", "application/json")
34 .json(&body)
35 .send()
36 .await
37 }
38
39 pub async fn post_raw(&self, path: &str, body: &str, content_type: &str) -> Result<reqwest::Response, reqwest::Error> {
40 self.client
41 .post(&format!("{}{}", self.base_url, path))
42 .header("Content-Type", content_type)
43 .body(body.to_string())
44 .send()
45 .await
46 }
47
48 pub async fn get(&self, path: &str) -> Result<reqwest::Response, reqwest::Error> {
49 self.client
50 .get(&format!("{}{}", self.base_url, path))
51 .send()
52 .await
53 }
54
55 pub async fn get_with_headers(&self, path: &str, headers: &[(&str, &str)]) -> Result<reqwest::Response, reqwest::Error> {
56 let mut request = self.client.get(&format!("{}{}", self.base_url, path));
57
58 for (key, value) in headers {
59 request = request.header(*key, *value);
60 }
61
62 request.send().await
63 }
64
65 pub async fn put_json(&self, path: &str, body: Value) -> Result<reqwest::Response, reqwest::Error> {
66 self.client
67 .put(&format!("{}{}", self.base_url, path))
68 .header("Content-Type", "application/json")
69 .json(&body)
70 .send()
71 .await
72 }
73
74 pub async fn delete(&self, path: &str) -> Result<reqwest::Response, reqwest::Error> {
75 self.client
76 .delete(&format!("{}{}", self.base_url, path))
77 .send()
78 .await
79 }
80}
81
82pub struct SecurityTestServer {
83 _handle: tokio::task::JoinHandle<()>,
84 base_url: String,
85}
86
87impl SecurityTestServer {
88 pub async fn start() -> Result<Self, Box<dyn std::error::Error>> {
89 use std::net::TcpListener;
90
91 let listener = TcpListener::bind("127.0.0.1:0")?;
92 let port = listener.local_addr()?.port();
93 drop(listener);
94
95 let graph = Arc::new(Graph::new());
96 let config = ServerConfig::default();
97 let server = RestServer::new(graph, config);
98
99 let handle = tokio::spawn(async move {
100 if let Err(e) = server.serve(port).await {
101 eprintln!("Security test server error: {}", e);
102 }
103 });
104
105 sleep(Duration::from_millis(100)).await;
106
107 let base_url = format!("http://localhost:{}", port);
108
109 Ok(Self {
110 _handle: handle,
111 base_url,
112 })
113 }
114
115 pub fn client(&self) -> SecurityTestClient {
116 SecurityTestClient::new(&self.base_url)
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 async fn setup() -> SecurityTestServer {
125 SecurityTestServer::start().await.expect("Failed to start test server")
126 }
127
128 #[tokio::test]
129 async fn test_sql_injection_protection() {
130 let server = setup().await;
131 let client = server.client();
132
133 // Test SQL injection attempts in various fields
134 let sql_injection_payloads = vec![
135 "'; DROP TABLE nodes; --",
136 "1' OR '1'='1",
137 "'; DELETE FROM users WHERE '1'='1; --",
138 "1'; UNION SELECT * FROM information_schema.tables; --",
139 "admin'--",
140 "' OR 1=1#",
141 "'; EXEC xp_cmdshell('dir'); --",
142 ];
143
144 for payload in sql_injection_payloads {
145 // Test in node creation
146 let malicious_node = json!({
147 "labels": ["TestNode"],
148 "properties": {
149 "name": payload,
150 "description": payload
151 }
152 });
153
154 let response = client.post_json("/api/v1/nodes", malicious_node).await.expect("Request failed");
155
156 // Should either accept the data as text (safe) or reject it gracefully
157 assert!(response.status() == 200 || response.status().is_client_error(),
158 "SQL injection payload should be handled safely");
159
160 if response.status() == 200 {
161 // If accepted, verify it's stored as literal text (not executed)
162 let body: Value = response.json().await.expect("Response parsing failed");
163 let node_id = body["node_id"].as_u64().expect("node_id should be present");
164
165 let get_response = client.get(&format!("/api/v1/nodes/{}", node_id)).await.expect("Get failed");
166 assert_eq!(get_response.status(), 200, "Node should be retrievable");
167
168 let get_body: Value = get_response.json().await.expect("Get response parsing failed");
169 assert_eq!(get_body["properties"]["name"], payload, "SQL injection should be stored as literal text");
170 }
171
172 // Test in constraint creation
173 let malicious_constraint = json!({
174 "constraint_type": "required",
175 "label": payload,
176 "property": payload
177 });
178
179 let response = client.post_json("/api/v1/constraints", malicious_constraint).await.expect("Request failed");
180 assert!(response.status().is_success() || response.status().is_client_error(),
181 "Constraint creation should handle SQL injection safely");
182 }
183 }
184
185 #[tokio::test]
186 async fn test_xss_protection() {
187 let server = setup().await;
188 let client = server.client();
189
190 // Test XSS payloads
191 let xss_payloads = vec![
192 "<script>alert('XSS')</script>",
193 "<img src=x onerror=alert('XSS')>",
194 "javascript:alert('XSS')",
195 "<svg onload=alert('XSS')>",
196 "';alert(String.fromCharCode(88,83,83))//';alert(String.fromCharCode(88,83,83))//\";alert(String.fromCharCode(88,83,83))//\";alert(String.fromCharCode(88,83,83))//-->\"'><svg/onload=alert(String.fromCharCode(88,83,83))>",
197 "<iframe src=\"javascript:alert('XSS')\"></iframe>",
198 ];
199
200 for payload in xss_payloads {
201 let xss_node = json!({
202 "labels": ["XSSTest"],
203 "properties": {
204 "content": payload,
205 "title": payload
206 }
207 });
208
209 let response = client.post_json("/api/v1/nodes", xss_node).await.expect("Request failed");
210
211 // Should handle XSS payloads safely
212 assert!(response.status() == 200 || response.status().is_client_error(),
213 "XSS payload should be handled safely");
214
215 if response.status() == 200 {
216 let body: Value = response.json().await.expect("Response parsing failed");
217 let node_id = body["node_id"].as_u64().expect("node_id should be present");
218
219 // Verify the payload is stored as text, not executed
220 let get_response = client.get(&format!("/api/v1/nodes/{}", node_id)).await.expect("Get failed");
221
222 // Verify Content-Type is JSON (not HTML that could execute scripts)
223 let content_type = get_response.headers().get("content-type")
224 .and_then(|v| v.to_str().ok())
225 .unwrap_or("");
226 assert!(content_type.contains("application/json"), "Response should be JSON, not HTML");
227
228 let get_body: Value = get_response.json().await.expect("Get response parsing failed");
229
230 // The content should be stored literally (escaped or as-is, but not executed)
231 assert!(get_body["properties"]["content"].is_string(), "XSS content should be stored as string");
232 }
233 }
234 }
235
236 #[tokio::test]
237 async fn test_large_payload_dos_protection() {
238 let server = setup().await;
239 let client = server.client();
240
241 // Test extremely large payloads that could cause DoS
242 let large_string = "A".repeat(10_000_000); // 10MB string
243
244 let large_payload = json!({
245 "labels": ["DoSTest"],
246 "properties": {
247 "large_field": large_string
248 }
249 });
250
251 let response = client.post_json("/api/v1/nodes", large_payload).await.expect("Request failed");
252
253 // Should either reject large payloads or handle them gracefully
254 assert!(response.status() == 200 ||
255 response.status() == 413 || // Payload Too Large
256 response.status() == 400 || // Bad Request
257 response.status() == 500, // Internal Server Error (acceptable for resource protection)
258 "Large payload should be handled safely, got: {}", response.status());
259
260 // If accepted, server should remain responsive
261 if response.status() == 200 {
262 // Verify server is still responsive with a simple request
263 let health_response = client.get("/health").await.expect("Health check failed");
264 assert_eq!(health_response.status(), 200, "Server should remain responsive after large payload");
265 }
266 }
267
268 #[tokio::test]
269 async fn test_malformed_json_bomb_protection() {
270 let server = setup().await;
271 let client = server.client();
272
273 // Test deeply nested JSON that could cause stack overflow
274 let mut nested_json = "{}".to_string();
275 for _ in 0..1000 {
276 nested_json = format!("{{\"nested\": {}}}", nested_json);
277 }
278
279 let response = client.post_raw("/api/v1/nodes", &nested_json, "application/json").await.expect("Request failed");
280
281 // Should handle deeply nested JSON safely
282 assert!(response.status().is_client_error() ||
283 response.status().is_server_error() ||
284 response.status() == 200,
285 "Deeply nested JSON should be handled safely");
286
287 // Test malformed JSON with many repeated keys (JSON bomb)
288 let json_bomb = format!("{{{}\"end\": \"value\"}}", "\"key\": {},".repeat(10000));
289
290 let response = client.post_raw("/api/v1/nodes", &json_bomb, "application/json").await.expect("Request failed");
291
292 assert!(response.status().is_client_error() ||
293 response.status().is_server_error() ||
294 response.status() == 200,
295 "JSON bomb should be handled safely");
296
297 // Verify server remains responsive
298 let health_response = client.get("/health").await.expect("Health check failed");
299 assert_eq!(health_response.status(), 200, "Server should remain responsive after JSON attacks");
300 }
301
302 #[tokio::test]
303 async fn test_path_traversal_protection() {
304 let server = setup().await;
305 let client = server.client();
306
307 // Test path traversal attempts
308 let path_traversal_payloads = vec![
309 "../../../etc/passwd",
310 "..\\..\\..\\windows\\system32\\config\\sam",
311 "%2e%2e%2f%2e%2e%2f%2e%2e%2f%65%74%63%2f%70%61%73%73%77%64",
312 "....//....//....//etc/passwd",
313 "/etc/passwd",
314 "C:\\Windows\\System32\\drivers\\etc\\hosts",
315 ];
316
317 for payload in path_traversal_payloads {
318 // Test in node ID parameter
319 let response = client.get(&format!("/api/v1/nodes/{}", payload)).await.expect("Request failed");
320
321 // Should return 404 or 400, not expose file system
322 assert!(response.status() == 404 || response.status() == 400,
323 "Path traversal attempt should return 404/400, got: {}", response.status());
324
325 // Verify no file system content is returned
326 if let Ok(body) = response.text().await {
327 assert!(!body.contains("root:"), "Response should not contain /etc/passwd content");
328 assert!(!body.contains("127.0.0.1"), "Response should not contain hosts file content");
329 }
330 }
331 }
332
333 #[tokio::test]
334 async fn test_http_method_security() {
335 let server = setup().await;
336 let client = server.client();
337
338 // Test HTTP method override attempts
339 let malicious_headers = vec![
340 ("X-HTTP-Method-Override", "DELETE"),
341 ("X-HTTP-Method", "PUT"),
342 ("X-Method-Override", "POST"),
343 ];
344
345 for (header_name, header_value) in malicious_headers {
346 // Try to override GET to DELETE using headers
347 let response = client.get_with_headers("/api/v1/nodes/1", &[(header_name, header_value)]).await.expect("Request failed");
348
349 // Should not honor method override headers (security risk)
350 // GET should remain GET, not become DELETE
351 assert!(response.status() == 404 || response.status() == 405 || response.status() == 200,
352 "Method override should not work: {}: {}", header_name, header_value);
353 }
354
355 // Test unsupported HTTP methods
356 let response = client.client
357 .request(reqwest::Method::TRACE, &format!("{}/api/v1/nodes", client.base_url))
358 .send()
359 .await
360 .expect("Request failed");
361
362 assert_eq!(response.status(), 405, "TRACE method should be disabled");
363
364 // Test CONNECT method
365 let response = client.client
366 .request(reqwest::Method::CONNECT, &format!("{}/api/v1/nodes", client.base_url))
367 .send()
368 .await
369 .expect("Request failed");
370
371 // CONNECT method should be rejected (405) or not found (404) - both are secure
372 assert!(response.status() == 405 || response.status() == 404,
373 "CONNECT method should be disabled, got: {}", response.status());
374 }
375
376 #[tokio::test]
377 async fn test_information_disclosure_protection() {
378 let server = setup().await;
379 let client = server.client();
380
381 // Test that error messages don't leak sensitive information
382 let response = client.get("/api/v1/nodes/999999999").await.expect("Request failed");
383 assert_eq!(response.status(), 404);
384
385 let error_body = response.text().await.expect("Response body failed");
386
387 // Error messages should not reveal:
388 assert!(!error_body.to_lowercase().contains("sql"), "Error should not reveal SQL details");
389 assert!(!error_body.to_lowercase().contains("database"), "Error should not reveal database details");
390 assert!(!error_body.to_lowercase().contains("stack trace"), "Error should not contain stack traces");
391 assert!(!error_body.to_lowercase().contains("internal"), "Error should not reveal internal details");
392 assert!(!error_body.contains("/home/"), "Error should not reveal file paths");
393 assert!(!error_body.contains("C:\\"), "Error should not reveal Windows paths");
394
395 // Test malformed requests don't leak info
396 let response = client.post_raw("/api/v1/nodes", "invalid json{{{", "application/json").await.expect("Request failed");
397 assert_eq!(response.status(), 400);
398
399 let error_body = response.text().await.expect("Response body failed");
400 assert!(!error_body.to_lowercase().contains("panic"), "Error should not reveal panic details");
401 assert!(!error_body.to_lowercase().contains("thread"), "Error should not reveal thread details");
402 }
403
404 #[tokio::test]
405 async fn test_input_validation_and_sanitization() {
406 let server = setup().await;
407 let client = server.client();
408
409 // Test null byte injection
410 let null_byte_payloads = vec![
411 "test\0.txt",
412 "normal_text\0",
413 "\0delete_everything",
414 ];
415
416 for payload in null_byte_payloads {
417 let node_with_null = json!({
418 "labels": ["NullTest"],
419 "properties": {
420 "content": payload
421 }
422 });
423
424 let response = client.post_json("/api/v1/nodes", node_with_null).await.expect("Request failed");
425
426 // Should handle null bytes safely
427 assert!(response.status() == 200 || response.status().is_client_error(),
428 "Null byte payload should be handled safely");
429
430 if response.status() == 200 {
431 let body: Value = response.json().await.expect("Response parsing failed");
432 let node_id = body["node_id"].as_u64().expect("node_id should be present");
433
434 // Verify null bytes are handled safely (either stripped or stored safely)
435 let get_response = client.get(&format!("/api/v1/nodes/{}", node_id)).await.expect("Get failed");
436 assert_eq!(get_response.status(), 200);
437 }
438 }
439
440 // Test extremely long field names
441 let long_field_name = "a".repeat(10000);
442 let long_field_node = json!({
443 "labels": ["LongFieldTest"],
444 "properties": {
445 long_field_name: "value"
446 }
447 });
448
449 let response = client.post_json("/api/v1/nodes", long_field_node).await.expect("Request failed");
450
451 // Should either accept or reject gracefully
452 assert!(response.status() == 200 || response.status().is_client_error(),
453 "Long field names should be handled safely");
454 }
455
456 #[tokio::test]
457 async fn test_rate_limiting_and_abuse_protection() {
458 let server = setup().await;
459 let client = server.client();
460
461 // Test rapid fire requests to simulate DoS
462 let mut responses = Vec::new();
463
464 for i in 0..100 {
465 let rapid_request = json!({
466 "labels": ["RateLimitTest"],
467 "properties": {
468 "request_id": i
469 }
470 });
471
472 let response = client.post_json("/api/v1/nodes", rapid_request).await.expect("Request failed");
473 responses.push(response.status());
474
475 // Small delay to avoid overwhelming the test
476 sleep(Duration::from_millis(1)).await;
477 }
478
479 // Check that the server handled rapid requests gracefully
480 let success_count = responses.iter().filter(|&&status| status == 200).count();
481 let error_count = responses.iter().filter(|&&status| status != 200).count();
482
483 // Server should either:
484 // 1. Handle all requests successfully (good performance)
485 // 2. Start rate limiting after some threshold (good protection)
486 assert!(success_count > 0, "At least some requests should succeed");
487
488 // If there are many errors, they should be proper rate limiting responses
489 if error_count > 50 {
490 let rate_limit_errors = responses.iter().filter(|&&status| status == 429).count();
491 // If rate limiting is implemented, should see 429 responses
492 println!("Rate limiting test: {} successes, {} errors, {} rate limits",
493 success_count, error_count, rate_limit_errors);
494 }
495
496 // Verify server is still responsive after rapid requests
497 let health_response = client.get("/health").await.expect("Health check failed");
498 assert_eq!(health_response.status(), 200, "Server should remain responsive after rapid requests");
499 }
500
501 #[tokio::test]
502 async fn test_content_type_validation() {
503 let server = setup().await;
504 let client = server.client();
505
506 // Test various content types that could be used maliciously
507 let malicious_content_types = vec![
508 "text/html",
509 "application/xml",
510 "text/xml",
511 "application/x-www-form-urlencoded",
512 "multipart/form-data",
513 "text/plain",
514 "application/octet-stream",
515 ];
516
517 let valid_json = json!({
518 "labels": ["ContentTypeTest"],
519 "properties": {
520 "test": "value"
521 }
522 }).to_string();
523
524 for content_type in malicious_content_types {
525 let response = client.post_raw("/api/v1/nodes", &valid_json, content_type).await.expect("Request failed");
526
527 // Should reject non-JSON content types for JSON endpoints
528 if content_type != "application/json" {
529 assert!(response.status().is_client_error(),
530 "Should reject content type: {}, got status: {}", content_type, response.status());
531 }
532 }
533
534 // Test content type injection
535 let injected_content_type = "application/json; charset=utf-8; boundary=--evil-boundary";
536 let response = client.post_raw("/api/v1/nodes", &valid_json, injected_content_type).await.expect("Request failed");
537
538 // Should either accept (if parsing charset only) or reject gracefully
539 assert!(response.status() == 200 || response.status().is_client_error(),
540 "Content type injection should be handled safely");
541 }
542
543 #[tokio::test]
544 async fn test_unicode_security_issues() {
545 let server = setup().await;
546 let client = server.client();
547
548 // Test Unicode normalization attacks
549 let unicode_attack_payloads = vec![
550 "\u{202E}overrightleft\u{202D}", // Right-to-left override
551 "\u{200B}\u{200C}\u{200D}invisible", // Zero-width characters
552 "нормал", // Cyrillic characters that look like Latin
553 "а", // Cyrillic 'a' that looks like Latin 'a'
554 "test\u{0000}null", // Null character in Unicode
555 "\u{FEFF}bomtest", // Byte Order Mark
556 ];
557
558 for payload in unicode_attack_payloads {
559 let unicode_node = json!({
560 "labels": ["UnicodeSecTest"],
561 "properties": {
562 "content": payload,
563 "name": payload
564 }
565 });
566
567 let response = client.post_json("/api/v1/nodes", unicode_node).await.expect("Request failed");
568
569 // Should handle Unicode attacks safely
570 assert!(response.status() == 200 || response.status().is_client_error(),
571 "Unicode attack should be handled safely: {:?}", payload);
572
573 if response.status() == 200 {
574 let body: Value = response.json().await.expect("Response parsing failed");
575 let node_id = body["node_id"].as_u64().expect("node_id should be present");
576
577 // Verify the content is stored safely
578 let get_response = client.get(&format!("/api/v1/nodes/{}", node_id)).await.expect("Get failed");
579 assert_eq!(get_response.status(), 200);
580
581 let get_body: Value = get_response.json().await.expect("Get response parsing failed");
582 assert!(get_body["properties"]["content"].is_string(), "Unicode content should be stored as string");
583 }
584 }
585 }
586
587 #[tokio::test]
588 async fn test_server_information_leakage() {
589 let server = setup().await;
590 let client = server.client();
591
592 // Test that server doesn't leak version or implementation details
593 let response = client.get("/health").await.expect("Request failed");
594
595 let headers = response.headers();
596
597 // Check for information leakage in headers
598 assert!(!headers.contains_key("server"), "Should not expose server information");
599 assert!(!headers.contains_key("x-powered-by"), "Should not expose technology stack");
600
601 // Check response doesn't contain sensitive info
602 let body = response.text().await.expect("Response body failed");
603 assert!(!body.to_lowercase().contains("rust"), "Should not expose implementation language");
604 assert!(!body.to_lowercase().contains("tokio"), "Should not expose framework details");
605 assert!(!body.to_lowercase().contains("axum"), "Should not expose framework details");
606
607 // Test 404 responses don't leak info
608 let not_found = client.get("/nonexistent/path/that/does/not/exist").await.expect("Request failed");
609 assert_eq!(not_found.status(), 404);
610
611 let not_found_body = not_found.text().await.expect("Response body failed");
612 assert!(!not_found_body.to_lowercase().contains("target/debug"), "Should not expose build paths");
613 assert!(!not_found_body.to_lowercase().contains("src/"), "Should not expose source paths");
614 }
615}