this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 615 lines 26 kB view raw
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}