mockforge_bench/
security_payloads.rs

1//! Security testing payloads for load testing
2//!
3//! This module provides built-in security testing payloads for common
4//! vulnerability categories including SQL injection, XSS, command injection,
5//! and path traversal.
6
7use crate::error::{BenchError, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashSet;
10use std::path::Path;
11
12/// Security payload categories
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "kebab-case")]
15pub enum SecurityCategory {
16    /// SQL Injection payloads
17    SqlInjection,
18    /// Cross-Site Scripting (XSS) payloads
19    Xss,
20    /// Command Injection payloads
21    CommandInjection,
22    /// Path Traversal payloads
23    PathTraversal,
24    /// Server-Side Template Injection
25    Ssti,
26    /// LDAP Injection
27    LdapInjection,
28    /// XML External Entity (XXE)
29    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/// A security testing payload
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct SecurityPayload {
66    /// The payload string to inject
67    pub payload: String,
68    /// Category of the payload
69    pub category: SecurityCategory,
70    /// Description of what this payload tests
71    pub description: String,
72    /// Whether this is considered a high-risk payload
73    pub high_risk: bool,
74}
75
76impl SecurityPayload {
77    /// Create a new security payload
78    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    /// Mark as high risk
88    pub fn high_risk(mut self) -> Self {
89        self.high_risk = true;
90        self
91    }
92}
93
94/// Configuration for security testing
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct SecurityTestConfig {
97    /// Whether security testing is enabled
98    pub enabled: bool,
99    /// Categories to test
100    pub categories: HashSet<SecurityCategory>,
101    /// Specific fields to target for injection
102    pub target_fields: Vec<String>,
103    /// Path to custom payloads file (extends built-in)
104    pub custom_payloads_file: Option<String>,
105    /// Whether to include high-risk payloads
106    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    /// Enable security testing
127    pub fn enable(mut self) -> Self {
128        self.enabled = true;
129        self
130    }
131
132    /// Set categories to test
133    pub fn with_categories(mut self, categories: HashSet<SecurityCategory>) -> Self {
134        self.categories = categories;
135        self
136    }
137
138    /// Set target fields
139    pub fn with_target_fields(mut self, fields: Vec<String>) -> Self {
140        self.target_fields = fields;
141        self
142    }
143
144    /// Set custom payloads file
145    pub fn with_custom_payloads(mut self, path: String) -> Self {
146        self.custom_payloads_file = Some(path);
147        self
148    }
149
150    /// Enable high-risk payloads
151    pub fn with_high_risk(mut self) -> Self {
152        self.include_high_risk = true;
153        self
154    }
155
156    /// Parse categories from a comma-separated string
157    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
166/// Built-in security payloads
167pub struct SecurityPayloads;
168
169impl SecurityPayloads {
170    /// Get SQL injection payloads
171    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    /// Get XSS payloads
213    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    /// Get command injection payloads
254    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    /// Get path traversal payloads
298    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    /// Get SSTI payloads
334    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    /// Get all payloads for a specific category
360    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(), // TODO: Add LDAP payloads
368            SecurityCategory::Xxe => Vec::new(),           // TODO: Add XXE payloads
369        }
370    }
371
372    /// Get all payloads for configured categories
373    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        // Filter out high-risk if not included
378        if !config.include_high_risk {
379            payloads.retain(|p| !p.high_risk);
380        }
381
382        payloads
383    }
384
385    /// Load custom payloads from a file
386    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
395/// Generates k6 JavaScript code for security testing
396pub struct SecurityTestGenerator;
397
398impl SecurityTestGenerator {
399    /// Generate k6 code for security payload selection
400    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            // Escape the payload for JavaScript
408            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    /// Generate k6 code for applying security payload to request
429    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    /// Generate k6 code for security test checks
462    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        // Should not include high-risk payloads by default
619        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        // Should include some high-risk payloads
628        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        // Single quotes should be escaped
677        assert!(code.contains("\\'"));
678    }
679}