1use crate::error::{BenchError, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashSet;
10use std::path::Path;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "kebab-case")]
15pub enum SecurityCategory {
16 SqlInjection,
18 Xss,
20 CommandInjection,
22 PathTraversal,
24 Ssti,
26 LdapInjection,
28 Xxe,
30}
31
32impl std::fmt::Display for SecurityCategory {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 Self::SqlInjection => write!(f, "sqli"),
36 Self::Xss => write!(f, "xss"),
37 Self::CommandInjection => write!(f, "command-injection"),
38 Self::PathTraversal => write!(f, "path-traversal"),
39 Self::Ssti => write!(f, "ssti"),
40 Self::LdapInjection => write!(f, "ldap-injection"),
41 Self::Xxe => write!(f, "xxe"),
42 }
43 }
44}
45
46impl std::str::FromStr for SecurityCategory {
47 type Err = String;
48
49 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
50 match s.to_lowercase().replace('_', "-").as_str() {
51 "sqli" | "sql-injection" | "sqlinjection" => Ok(Self::SqlInjection),
52 "xss" | "cross-site-scripting" => Ok(Self::Xss),
53 "command-injection" | "commandinjection" | "cmd" => Ok(Self::CommandInjection),
54 "path-traversal" | "pathtraversal" | "lfi" => Ok(Self::PathTraversal),
55 "ssti" | "template-injection" => Ok(Self::Ssti),
56 "ldap-injection" | "ldapinjection" | "ldap" => Ok(Self::LdapInjection),
57 "xxe" | "xml-external-entity" => Ok(Self::Xxe),
58 _ => Err(format!("Unknown security category: '{}'", s)),
59 }
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct SecurityPayload {
66 pub payload: String,
68 pub category: SecurityCategory,
70 pub description: String,
72 pub high_risk: bool,
74}
75
76impl SecurityPayload {
77 pub fn new(payload: String, category: SecurityCategory, description: String) -> Self {
79 Self {
80 payload,
81 category,
82 description,
83 high_risk: false,
84 }
85 }
86
87 pub fn high_risk(mut self) -> Self {
89 self.high_risk = true;
90 self
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct SecurityTestConfig {
97 pub enabled: bool,
99 pub categories: HashSet<SecurityCategory>,
101 pub target_fields: Vec<String>,
103 pub custom_payloads_file: Option<String>,
105 pub include_high_risk: bool,
107}
108
109impl Default for SecurityTestConfig {
110 fn default() -> Self {
111 let mut categories = HashSet::new();
112 categories.insert(SecurityCategory::SqlInjection);
113 categories.insert(SecurityCategory::Xss);
114
115 Self {
116 enabled: false,
117 categories,
118 target_fields: Vec::new(),
119 custom_payloads_file: None,
120 include_high_risk: false,
121 }
122 }
123}
124
125impl SecurityTestConfig {
126 pub fn enable(mut self) -> Self {
128 self.enabled = true;
129 self
130 }
131
132 pub fn with_categories(mut self, categories: HashSet<SecurityCategory>) -> Self {
134 self.categories = categories;
135 self
136 }
137
138 pub fn with_target_fields(mut self, fields: Vec<String>) -> Self {
140 self.target_fields = fields;
141 self
142 }
143
144 pub fn with_custom_payloads(mut self, path: String) -> Self {
146 self.custom_payloads_file = Some(path);
147 self
148 }
149
150 pub fn with_high_risk(mut self) -> Self {
152 self.include_high_risk = true;
153 self
154 }
155
156 pub fn parse_categories(s: &str) -> std::result::Result<HashSet<SecurityCategory>, String> {
158 if s.is_empty() {
159 return Ok(HashSet::new());
160 }
161
162 s.split(',').map(|c| c.trim().parse::<SecurityCategory>()).collect()
163 }
164}
165
166pub struct SecurityPayloads;
168
169impl SecurityPayloads {
170 pub fn sql_injection() -> Vec<SecurityPayload> {
172 vec![
173 SecurityPayload::new(
174 "' OR '1'='1".to_string(),
175 SecurityCategory::SqlInjection,
176 "Basic SQL injection tautology".to_string(),
177 ),
178 SecurityPayload::new(
179 "' OR '1'='1' --".to_string(),
180 SecurityCategory::SqlInjection,
181 "SQL injection with comment".to_string(),
182 ),
183 SecurityPayload::new(
184 "'; DROP TABLE users; --".to_string(),
185 SecurityCategory::SqlInjection,
186 "SQL injection table drop attempt".to_string(),
187 )
188 .high_risk(),
189 SecurityPayload::new(
190 "' UNION SELECT * FROM users --".to_string(),
191 SecurityCategory::SqlInjection,
192 "SQL injection union-based data extraction".to_string(),
193 ),
194 SecurityPayload::new(
195 "1' AND '1'='1".to_string(),
196 SecurityCategory::SqlInjection,
197 "SQL injection boolean-based blind".to_string(),
198 ),
199 SecurityPayload::new(
200 "1; WAITFOR DELAY '0:0:5' --".to_string(),
201 SecurityCategory::SqlInjection,
202 "SQL injection time-based blind (MSSQL)".to_string(),
203 ),
204 SecurityPayload::new(
205 "1' AND SLEEP(5) --".to_string(),
206 SecurityCategory::SqlInjection,
207 "SQL injection time-based blind (MySQL)".to_string(),
208 ),
209 ]
210 }
211
212 pub fn xss() -> Vec<SecurityPayload> {
214 vec![
215 SecurityPayload::new(
216 "<script>alert('XSS')</script>".to_string(),
217 SecurityCategory::Xss,
218 "Basic script tag XSS".to_string(),
219 ),
220 SecurityPayload::new(
221 "<img src=x onerror=alert('XSS')>".to_string(),
222 SecurityCategory::Xss,
223 "Image tag XSS with onerror".to_string(),
224 ),
225 SecurityPayload::new(
226 "<svg/onload=alert('XSS')>".to_string(),
227 SecurityCategory::Xss,
228 "SVG tag XSS with onload".to_string(),
229 ),
230 SecurityPayload::new(
231 "javascript:alert('XSS')".to_string(),
232 SecurityCategory::Xss,
233 "JavaScript protocol XSS".to_string(),
234 ),
235 SecurityPayload::new(
236 "<body onload=alert('XSS')>".to_string(),
237 SecurityCategory::Xss,
238 "Body tag XSS with onload".to_string(),
239 ),
240 SecurityPayload::new(
241 "'><script>alert(String.fromCharCode(88,83,83))</script>".to_string(),
242 SecurityCategory::Xss,
243 "XSS with character encoding".to_string(),
244 ),
245 SecurityPayload::new(
246 "<div style=\"background:url(javascript:alert('XSS'))\">".to_string(),
247 SecurityCategory::Xss,
248 "CSS-based XSS".to_string(),
249 ),
250 ]
251 }
252
253 pub fn command_injection() -> Vec<SecurityPayload> {
255 vec![
256 SecurityPayload::new(
257 "; ls -la".to_string(),
258 SecurityCategory::CommandInjection,
259 "Unix command injection with semicolon".to_string(),
260 ),
261 SecurityPayload::new(
262 "| cat /etc/passwd".to_string(),
263 SecurityCategory::CommandInjection,
264 "Unix command injection with pipe".to_string(),
265 )
266 .high_risk(),
267 SecurityPayload::new(
268 "$(cat /etc/passwd)".to_string(),
269 SecurityCategory::CommandInjection,
270 "Unix command substitution".to_string(),
271 )
272 .high_risk(),
273 SecurityPayload::new(
274 "`cat /etc/passwd`".to_string(),
275 SecurityCategory::CommandInjection,
276 "Unix backtick command execution".to_string(),
277 )
278 .high_risk(),
279 SecurityPayload::new(
280 "& dir".to_string(),
281 SecurityCategory::CommandInjection,
282 "Windows command injection".to_string(),
283 ),
284 SecurityPayload::new(
285 "|| ping -c 3 127.0.0.1".to_string(),
286 SecurityCategory::CommandInjection,
287 "Command injection with OR".to_string(),
288 ),
289 SecurityPayload::new(
290 "\n/bin/sh -c 'echo vulnerable'".to_string(),
291 SecurityCategory::CommandInjection,
292 "Newline-based command injection".to_string(),
293 ),
294 ]
295 }
296
297 pub fn path_traversal() -> Vec<SecurityPayload> {
299 vec![
300 SecurityPayload::new(
301 "../../../etc/passwd".to_string(),
302 SecurityCategory::PathTraversal,
303 "Basic path traversal".to_string(),
304 ),
305 SecurityPayload::new(
306 "..%2F..%2F..%2Fetc%2Fpasswd".to_string(),
307 SecurityCategory::PathTraversal,
308 "URL-encoded path traversal".to_string(),
309 ),
310 SecurityPayload::new(
311 "....//....//....//etc/passwd".to_string(),
312 SecurityCategory::PathTraversal,
313 "Double-dot path traversal bypass".to_string(),
314 ),
315 SecurityPayload::new(
316 "..%252f..%252f..%252fetc%252fpasswd".to_string(),
317 SecurityCategory::PathTraversal,
318 "Double URL-encoded path traversal".to_string(),
319 ),
320 SecurityPayload::new(
321 "/etc/passwd%00.jpg".to_string(),
322 SecurityCategory::PathTraversal,
323 "Null byte injection path traversal".to_string(),
324 ),
325 SecurityPayload::new(
326 "....\\....\\....\\windows\\system32\\config\\sam".to_string(),
327 SecurityCategory::PathTraversal,
328 "Windows path traversal".to_string(),
329 ),
330 ]
331 }
332
333 pub fn ssti() -> Vec<SecurityPayload> {
335 vec![
336 SecurityPayload::new(
337 "{{7*7}}".to_string(),
338 SecurityCategory::Ssti,
339 "Jinja2/Twig SSTI detection".to_string(),
340 ),
341 SecurityPayload::new(
342 "${7*7}".to_string(),
343 SecurityCategory::Ssti,
344 "Freemarker SSTI detection".to_string(),
345 ),
346 SecurityPayload::new(
347 "<%= 7*7 %>".to_string(),
348 SecurityCategory::Ssti,
349 "ERB SSTI detection".to_string(),
350 ),
351 SecurityPayload::new(
352 "#{7*7}".to_string(),
353 SecurityCategory::Ssti,
354 "Ruby SSTI detection".to_string(),
355 ),
356 ]
357 }
358
359 pub fn get_by_category(category: SecurityCategory) -> Vec<SecurityPayload> {
361 match category {
362 SecurityCategory::SqlInjection => Self::sql_injection(),
363 SecurityCategory::Xss => Self::xss(),
364 SecurityCategory::CommandInjection => Self::command_injection(),
365 SecurityCategory::PathTraversal => Self::path_traversal(),
366 SecurityCategory::Ssti => Self::ssti(),
367 SecurityCategory::LdapInjection => Vec::new(), SecurityCategory::Xxe => Vec::new(), }
370 }
371
372 pub fn get_payloads(config: &SecurityTestConfig) -> Vec<SecurityPayload> {
374 let mut payloads: Vec<SecurityPayload> =
375 config.categories.iter().flat_map(|c| Self::get_by_category(*c)).collect();
376
377 if !config.include_high_risk {
379 payloads.retain(|p| !p.high_risk);
380 }
381
382 payloads
383 }
384
385 pub fn load_custom_payloads(path: &Path) -> Result<Vec<SecurityPayload>> {
387 let content = std::fs::read_to_string(path)
388 .map_err(|e| BenchError::Other(format!("Failed to read payloads file: {}", e)))?;
389
390 serde_json::from_str(&content)
391 .map_err(|e| BenchError::Other(format!("Failed to parse payloads file: {}", e)))
392 }
393}
394
395pub struct SecurityTestGenerator;
397
398impl SecurityTestGenerator {
399 pub fn generate_payload_selection(payloads: &[SecurityPayload]) -> String {
401 let mut code = String::new();
402
403 code.push_str("// Security testing payloads\n");
404 code.push_str("const securityPayloads = [\n");
405
406 for payload in payloads {
407 let escaped = payload
409 .payload
410 .replace('\\', "\\\\")
411 .replace('\'', "\\'")
412 .replace('\n', "\\n")
413 .replace('\r', "\\r");
414
415 code.push_str(&format!(
416 " {{ payload: '{}', category: '{}', description: '{}' }},\n",
417 escaped, payload.category, payload.description
418 ));
419 }
420
421 code.push_str("];\n\n");
422 code.push_str("// Select random security payload\n");
423 code.push_str("const securityPayload = securityPayloads[Math.floor(Math.random() * securityPayloads.length)];\n");
424
425 code
426 }
427
428 pub fn generate_apply_payload(target_fields: &[String]) -> String {
430 let mut code = String::new();
431
432 code.push_str("// Apply security payload to request\n");
433 code.push_str("function applySecurityPayload(payload, targetFields, secPayload) {\n");
434 code.push_str(" const result = { ...payload };\n");
435 code.push_str(" \n");
436
437 if target_fields.is_empty() {
438 code.push_str(" // No specific target fields - inject into first string field\n");
439 code.push_str(" for (const key of Object.keys(result)) {\n");
440 code.push_str(" if (typeof result[key] === 'string') {\n");
441 code.push_str(" result[key] = secPayload.payload;\n");
442 code.push_str(" break;\n");
443 code.push_str(" }\n");
444 code.push_str(" }\n");
445 } else {
446 code.push_str(" // Inject into specified target fields\n");
447 code.push_str(" for (const field of targetFields) {\n");
448 code.push_str(" if (result.hasOwnProperty(field)) {\n");
449 code.push_str(" result[field] = secPayload.payload;\n");
450 code.push_str(" }\n");
451 code.push_str(" }\n");
452 }
453
454 code.push_str(" \n");
455 code.push_str(" return result;\n");
456 code.push_str("}\n");
457
458 code
459 }
460
461 pub fn generate_security_checks() -> String {
463 r#"// Security test response checks
464function checkSecurityResponse(res, expectedVulnerable) {
465 // Check for common vulnerability indicators
466 const body = res.body || '';
467
468 const vulnerabilityIndicators = [
469 // SQL injection
470 'SQL syntax',
471 'mysql_fetch',
472 'ORA-',
473 'PostgreSQL',
474
475 // Command injection
476 'root:',
477 '/bin/',
478 'uid=',
479
480 // Path traversal
481 '[extensions]',
482 'passwd',
483
484 // XSS (reflected)
485 '<script>alert',
486 'onerror=',
487
488 // Error disclosure
489 'stack trace',
490 'Exception',
491 'Error in',
492 ];
493
494 const foundIndicator = vulnerabilityIndicators.some(ind =>
495 body.toLowerCase().includes(ind.toLowerCase())
496 );
497
498 if (foundIndicator) {
499 console.warn(`POTENTIAL VULNERABILITY: ${securityPayload.description}`);
500 console.warn(`Category: ${securityPayload.category}`);
501 console.warn(`Status: ${res.status}`);
502 }
503
504 return check(res, {
505 'security test: no obvious vulnerability': () => !foundIndicator,
506 'security test: proper error handling': (r) => r.status < 500,
507 });
508}
509"#
510 .to_string()
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517 use std::str::FromStr;
518
519 #[test]
520 fn test_security_category_display() {
521 assert_eq!(SecurityCategory::SqlInjection.to_string(), "sqli");
522 assert_eq!(SecurityCategory::Xss.to_string(), "xss");
523 assert_eq!(SecurityCategory::CommandInjection.to_string(), "command-injection");
524 assert_eq!(SecurityCategory::PathTraversal.to_string(), "path-traversal");
525 }
526
527 #[test]
528 fn test_security_category_from_str() {
529 assert_eq!(SecurityCategory::from_str("sqli").unwrap(), SecurityCategory::SqlInjection);
530 assert_eq!(
531 SecurityCategory::from_str("sql-injection").unwrap(),
532 SecurityCategory::SqlInjection
533 );
534 assert_eq!(SecurityCategory::from_str("xss").unwrap(), SecurityCategory::Xss);
535 assert_eq!(
536 SecurityCategory::from_str("command-injection").unwrap(),
537 SecurityCategory::CommandInjection
538 );
539 }
540
541 #[test]
542 fn test_security_category_from_str_invalid() {
543 assert!(SecurityCategory::from_str("invalid").is_err());
544 }
545
546 #[test]
547 fn test_security_test_config_default() {
548 let config = SecurityTestConfig::default();
549 assert!(!config.enabled);
550 assert!(config.categories.contains(&SecurityCategory::SqlInjection));
551 assert!(config.categories.contains(&SecurityCategory::Xss));
552 assert!(!config.include_high_risk);
553 }
554
555 #[test]
556 fn test_security_test_config_builders() {
557 let mut categories = HashSet::new();
558 categories.insert(SecurityCategory::CommandInjection);
559
560 let config = SecurityTestConfig::default()
561 .enable()
562 .with_categories(categories)
563 .with_target_fields(vec!["name".to_string()])
564 .with_high_risk();
565
566 assert!(config.enabled);
567 assert!(config.categories.contains(&SecurityCategory::CommandInjection));
568 assert!(!config.categories.contains(&SecurityCategory::SqlInjection));
569 assert_eq!(config.target_fields, vec!["name"]);
570 assert!(config.include_high_risk);
571 }
572
573 #[test]
574 fn test_parse_categories() {
575 let categories = SecurityTestConfig::parse_categories("sqli,xss,path-traversal").unwrap();
576 assert_eq!(categories.len(), 3);
577 assert!(categories.contains(&SecurityCategory::SqlInjection));
578 assert!(categories.contains(&SecurityCategory::Xss));
579 assert!(categories.contains(&SecurityCategory::PathTraversal));
580 }
581
582 #[test]
583 fn test_sql_injection_payloads() {
584 let payloads = SecurityPayloads::sql_injection();
585 assert!(!payloads.is_empty());
586 assert!(payloads.iter().all(|p| p.category == SecurityCategory::SqlInjection));
587 assert!(payloads.iter().any(|p| p.payload.contains("OR")));
588 }
589
590 #[test]
591 fn test_xss_payloads() {
592 let payloads = SecurityPayloads::xss();
593 assert!(!payloads.is_empty());
594 assert!(payloads.iter().all(|p| p.category == SecurityCategory::Xss));
595 assert!(payloads.iter().any(|p| p.payload.contains("<script>")));
596 }
597
598 #[test]
599 fn test_command_injection_payloads() {
600 let payloads = SecurityPayloads::command_injection();
601 assert!(!payloads.is_empty());
602 assert!(payloads.iter().all(|p| p.category == SecurityCategory::CommandInjection));
603 }
604
605 #[test]
606 fn test_path_traversal_payloads() {
607 let payloads = SecurityPayloads::path_traversal();
608 assert!(!payloads.is_empty());
609 assert!(payloads.iter().all(|p| p.category == SecurityCategory::PathTraversal));
610 assert!(payloads.iter().any(|p| p.payload.contains("..")));
611 }
612
613 #[test]
614 fn test_get_payloads_filters_high_risk() {
615 let config = SecurityTestConfig::default();
616 let payloads = SecurityPayloads::get_payloads(&config);
617
618 assert!(payloads.iter().all(|p| !p.high_risk));
620 }
621
622 #[test]
623 fn test_get_payloads_includes_high_risk() {
624 let config = SecurityTestConfig::default().with_high_risk();
625 let payloads = SecurityPayloads::get_payloads(&config);
626
627 assert!(payloads.iter().any(|p| p.high_risk));
629 }
630
631 #[test]
632 fn test_generate_payload_selection() {
633 let payloads = vec![SecurityPayload::new(
634 "' OR '1'='1".to_string(),
635 SecurityCategory::SqlInjection,
636 "Basic SQLi".to_string(),
637 )];
638
639 let code = SecurityTestGenerator::generate_payload_selection(&payloads);
640 assert!(code.contains("securityPayloads"));
641 assert!(code.contains("OR"));
642 assert!(code.contains("Math.random()"));
643 }
644
645 #[test]
646 fn test_generate_apply_payload_no_targets() {
647 let code = SecurityTestGenerator::generate_apply_payload(&[]);
648 assert!(code.contains("applySecurityPayload"));
649 assert!(code.contains("first string field"));
650 }
651
652 #[test]
653 fn test_generate_apply_payload_with_targets() {
654 let code = SecurityTestGenerator::generate_apply_payload(&["name".to_string()]);
655 assert!(code.contains("applySecurityPayload"));
656 assert!(code.contains("target fields"));
657 }
658
659 #[test]
660 fn test_generate_security_checks() {
661 let code = SecurityTestGenerator::generate_security_checks();
662 assert!(code.contains("checkSecurityResponse"));
663 assert!(code.contains("vulnerabilityIndicators"));
664 assert!(code.contains("POTENTIAL VULNERABILITY"));
665 }
666
667 #[test]
668 fn test_payload_escaping() {
669 let payloads = vec![SecurityPayload::new(
670 "'; DROP TABLE users; --".to_string(),
671 SecurityCategory::SqlInjection,
672 "Drop table".to_string(),
673 )];
674
675 let code = SecurityTestGenerator::generate_payload_selection(&payloads);
676 assert!(code.contains("\\'"));
678 }
679}