1use crate::detection_helpers::AppCharacteristics;
65use crate::http_client::HttpClient;
66use crate::types::{Confidence, ScanConfig, Severity, Vulnerability};
67use serde_json::{json, Value};
68use std::collections::HashSet;
69use std::sync::Arc;
70use tracing::{debug, info};
71
72pub struct AdvancedMassAssignmentScanner {
74 http_client: Arc<HttpClient>,
75 test_marker: String,
76}
77
78#[derive(Debug, Clone, PartialEq)]
80pub enum ApiFramework {
81 Rails,
82 Django,
83 Express,
84 Spring,
85 Laravel,
86 FastAPI,
87 Flask,
88 AspNet,
89 Unknown,
90}
91
92#[derive(Debug, Clone, PartialEq)]
94pub enum ContentType {
95 Json,
96 FormData,
97 Multipart,
98 Unknown,
99}
100
101#[derive(Debug, Clone)]
103pub struct EndpointContext {
104 pub framework: ApiFramework,
105 pub content_type: ContentType,
106 pub is_data_modification: bool,
107 pub extracted_fields: HashSet<String>,
108 pub has_user_context: bool,
109 pub has_auth: bool,
110}
111
112impl Default for EndpointContext {
113 fn default() -> Self {
114 Self {
115 framework: ApiFramework::Unknown,
116 content_type: ContentType::Unknown,
117 is_data_modification: false,
118 extracted_fields: HashSet::new(),
119 has_user_context: false,
120 has_auth: false,
121 }
122 }
123}
124
125impl AdvancedMassAssignmentScanner {
126 pub fn new(http_client: Arc<HttpClient>) -> Self {
127 let test_marker = format!("maa_{}", generate_uuid());
128 Self {
129 http_client,
130 test_marker,
131 }
132 }
133
134 pub async fn scan(
136 &self,
137 url: &str,
138 _config: &ScanConfig,
139 ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
140 if !crate::license::is_feature_available("mass_assignment_advanced") {
142 debug!("Advanced mass assignment scanner requires premium license");
143 return Ok((Vec::new(), 0));
144 }
145
146 let mut vulnerabilities = Vec::new();
147 let mut tests_run = 0;
148
149 info!("[MassAssignment-Advanced] Starting context-aware mass assignment scan");
150
151 let context = self.detect_endpoint_context(url).await;
153 debug!(
154 "[MassAssignment-Advanced] Detected context: framework={:?}, content_type={:?}, is_data_mod={}",
155 context.framework, context.content_type, context.is_data_modification
156 );
157
158 let (vulns, tests) = self.test_nested_object_injection(url, &context).await?;
160 vulnerabilities.extend(vulns);
161 tests_run += tests;
162
163 if vulnerabilities.is_empty() {
165 let (vulns, tests) = self.test_dot_notation_pollution(url, &context).await?;
166 vulnerabilities.extend(vulns);
167 tests_run += tests;
168 }
169
170 if vulnerabilities.is_empty() {
172 let (vulns, tests) = self.test_array_parameter_pollution(url, &context).await?;
173 vulnerabilities.extend(vulns);
174 tests_run += tests;
175 }
176
177 if vulnerabilities.is_empty() && context.content_type == ContentType::Json {
179 let (vulns, tests) = self.test_json_deep_merge(url, &context).await?;
180 vulnerabilities.extend(vulns);
181 tests_run += tests;
182 }
183
184 if vulnerabilities.is_empty() {
186 let (vulns, tests) = self.test_framework_specific(url, &context).await?;
187 vulnerabilities.extend(vulns);
188 tests_run += tests;
189 }
190
191 if vulnerabilities.is_empty() {
193 let (vulns, tests) = self.test_privilege_escalation_fields(url, &context).await?;
194 vulnerabilities.extend(vulns);
195 tests_run += tests;
196 }
197
198 if vulnerabilities.is_empty() {
200 let (vulns, tests) = self.test_ownership_manipulation(url, &context).await?;
201 vulnerabilities.extend(vulns);
202 tests_run += tests;
203 }
204
205 if vulnerabilities.is_empty() {
207 let (vulns, tests) = self
208 .test_financial_field_manipulation(url, &context)
209 .await?;
210 vulnerabilities.extend(vulns);
211 tests_run += tests;
212 }
213
214 if vulnerabilities.is_empty() {
216 let (vulns, tests) = self.test_timestamp_manipulation(url, &context).await?;
217 vulnerabilities.extend(vulns);
218 tests_run += tests;
219 }
220
221 if vulnerabilities.is_empty() {
222 info!(
223 "[MassAssignment-Advanced] No vulnerabilities found after {} tests",
224 tests_run
225 );
226 } else {
227 info!(
228 "[MassAssignment-Advanced] Found {} vulnerabilities",
229 vulnerabilities.len()
230 );
231 }
232
233 Ok((vulnerabilities, tests_run))
234 }
235
236 pub async fn scan_with_context(
238 &self,
239 url: &str,
240 config: &ScanConfig,
241 characteristics: &AppCharacteristics,
242 ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
243 if !characteristics.is_api && !self.looks_like_data_endpoint(url) {
245 debug!(
246 "[MassAssignment-Advanced] Skipping non-API endpoint: {}",
247 url
248 );
249 return Ok((Vec::new(), 0));
250 }
251
252 if characteristics.is_static && !characteristics.is_api {
254 debug!("[MassAssignment-Advanced] Skipping static site: {}", url);
255 return Ok((Vec::new(), 0));
256 }
257
258 self.scan(url, config).await
259 }
260
261 async fn detect_endpoint_context(&self, url: &str) -> EndpointContext {
263 let mut context = EndpointContext::default();
264
265 if let Ok(response) = self.http_client.get(url).await {
267 let body = &response.body;
268 let headers = &response.headers;
269
270 if let Some(ct) = headers.get("content-type") {
272 let ct_lower = ct.to_lowercase();
273 if ct_lower.contains("application/json") {
274 context.content_type = ContentType::Json;
275 } else if ct_lower.contains("application/x-www-form-urlencoded") {
276 context.content_type = ContentType::FormData;
277 } else if ct_lower.contains("multipart/form-data") {
278 context.content_type = ContentType::Multipart;
279 }
280 }
281
282 context.framework = self.detect_framework(headers, body);
284
285 if let Ok(json) = serde_json::from_str::<Value>(body) {
287 context.extracted_fields = self.extract_field_names(&json);
288 }
289
290 let body_lower = body.to_lowercase();
292 context.has_user_context = body_lower.contains("user")
293 || body_lower.contains("profile")
294 || body_lower.contains("account");
295
296 context.has_auth = headers.contains_key("authorization")
298 || headers.contains_key("x-auth-token")
299 || body_lower.contains("token")
300 || body_lower.contains("session");
301 }
302
303 context.is_data_modification = self.is_data_modification_endpoint(url);
305
306 context
307 }
308
309 fn detect_framework(
311 &self,
312 headers: &std::collections::HashMap<String, String>,
313 body: &str,
314 ) -> ApiFramework {
315 let body_lower = body.to_lowercase();
316
317 if let Some(server) = headers.get("server") {
319 let server_lower = server.to_lowercase();
320 if server_lower.contains("passenger") || server_lower.contains("puma") {
321 return ApiFramework::Rails;
322 }
323 if server_lower.contains("gunicorn") || server_lower.contains("uvicorn") {
324 if body_lower.contains("fastapi") {
325 return ApiFramework::FastAPI;
326 }
327 return ApiFramework::Django;
328 }
329 if server_lower.contains("werkzeug") {
330 return ApiFramework::Flask;
331 }
332 if server_lower.contains("kestrel") || server_lower.contains("iis") {
333 return ApiFramework::AspNet;
334 }
335 }
336
337 if let Some(powered_by) = headers.get("x-powered-by") {
339 let powered_lower = powered_by.to_lowercase();
340 if powered_lower.contains("express") {
341 return ApiFramework::Express;
342 }
343 if powered_lower.contains("php") {
344 return ApiFramework::Laravel; }
346 if powered_lower.contains("asp.net") {
347 return ApiFramework::AspNet;
348 }
349 }
350
351 if body_lower.contains("actioncontroller") || body_lower.contains("rails") {
353 return ApiFramework::Rails;
354 }
355 if body_lower.contains("django") || body_lower.contains("drf") {
356 return ApiFramework::Django;
357 }
358 if body_lower.contains("laravel") || body_lower.contains("lumen") {
359 return ApiFramework::Laravel;
360 }
361 if body_lower.contains("spring") || body_lower.contains("jackson") {
362 return ApiFramework::Spring;
363 }
364
365 ApiFramework::Unknown
366 }
367
368 fn extract_field_names(&self, json: &Value) -> HashSet<String> {
370 let mut fields = HashSet::new();
371 self.extract_fields_recursive(json, "", &mut fields);
372 fields
373 }
374
375 fn extract_fields_recursive(&self, json: &Value, prefix: &str, fields: &mut HashSet<String>) {
376 match json {
377 Value::Object(map) => {
378 for (key, value) in map {
379 let field_name = if prefix.is_empty() {
380 key.clone()
381 } else {
382 format!("{}.{}", prefix, key)
383 };
384 fields.insert(key.clone());
385 fields.insert(field_name.clone());
386 self.extract_fields_recursive(value, &field_name, fields);
387 }
388 }
389 Value::Array(arr) => {
390 for item in arr {
391 self.extract_fields_recursive(item, prefix, fields);
392 }
393 }
394 _ => {}
395 }
396 }
397
398 fn is_data_modification_endpoint(&self, url: &str) -> bool {
400 let url_lower = url.to_lowercase();
401 let modification_patterns = [
402 "/create",
403 "/update",
404 "/edit",
405 "/save",
406 "/register",
407 "/signup",
408 "/profile",
409 "/settings",
410 "/account",
411 "/user",
412 "/admin",
413 "/checkout",
414 "/order",
415 "/payment",
416 "/subscription",
417 ];
418
419 modification_patterns.iter().any(|p| url_lower.contains(p))
420 }
421
422 fn looks_like_data_endpoint(&self, url: &str) -> bool {
424 let url_lower = url.to_lowercase();
425 url_lower.contains("/api/")
426 || url_lower.contains("/v1/")
427 || url_lower.contains("/v2/")
428 || url_lower.contains("/rest/")
429 || url_lower.contains("/graphql")
430 || self.is_data_modification_endpoint(url)
431 }
432
433 async fn test_nested_object_injection(
438 &self,
439 url: &str,
440 context: &EndpointContext,
441 ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
442 let mut vulnerabilities = Vec::new();
443 let tests_run;
444
445 info!("[MassAssignment-Advanced] Testing nested object injection");
446
447 let url_payloads = self.generate_nested_url_payloads();
449 tests_run = url_payloads.len();
450
451 for (payload, technique) in &url_payloads {
452 let test_url = if url.contains('?') {
453 format!("{}&{}", url, payload)
454 } else {
455 format!("{}?{}", url, payload)
456 };
457
458 match self.http_client.get(&test_url).await {
459 Ok(response) => {
460 if self.verify_nested_injection(&response.body, payload) {
461 info!(
462 "[MassAssignment-Advanced] Nested object injection detected: {}",
463 technique
464 );
465 vulnerabilities.push(self.create_vulnerability(
466 url,
467 "Advanced Nested Object Injection",
468 payload,
469 &format!(
470 "Deep nested object properties can be injected via mass assignment. \
471 Technique: {}. Framework: {:?}",
472 technique, context.framework
473 ),
474 &format!("Successfully injected nested property using {}", technique),
475 Severity::Critical,
476 Confidence::High,
477 "CWE-915",
478 9.0,
479 ));
480 return Ok((vulnerabilities, tests_run));
481 }
482 }
483 Err(e) => {
484 debug!("Nested object test request failed: {}", e);
485 }
486 }
487 }
488
489 if context.content_type == ContentType::Json || context.content_type == ContentType::Unknown
491 {
492 let json_payloads = self.generate_nested_json_payloads();
493
494 for (payload, technique) in json_payloads {
495 let headers = vec![("Content-Type".to_string(), "application/json".to_string())];
496
497 match self
498 .http_client
499 .post_with_headers(url, &payload.to_string(), headers)
500 .await
501 {
502 Ok(response) => {
503 if self.verify_json_injection(&response.body, &payload) {
504 info!(
505 "[MassAssignment-Advanced] JSON nested injection detected: {}",
506 technique
507 );
508 vulnerabilities.push(self.create_vulnerability(
509 url,
510 "Advanced JSON Nested Object Injection",
511 &payload.to_string(),
512 &format!(
513 "Deep nested JSON objects can be injected via mass assignment. \
514 Technique: {}. Framework: {:?}",
515 technique, context.framework
516 ),
517 &format!(
518 "Successfully injected nested JSON property using {}",
519 technique
520 ),
521 Severity::Critical,
522 Confidence::High,
523 "CWE-915",
524 9.0,
525 ));
526 return Ok((vulnerabilities, tests_run));
527 }
528 }
529 Err(e) => {
530 debug!("JSON nested test request failed: {}", e);
531 }
532 }
533 }
534 }
535
536 Ok((vulnerabilities, tests_run))
537 }
538
539 fn generate_nested_url_payloads(&self) -> Vec<(String, String)> {
540 vec![
541 (
543 "user[role]=admin".to_string(),
544 "2-level: user[role]".to_string(),
545 ),
546 (
547 "user[isAdmin]=true".to_string(),
548 "2-level: user[isAdmin]".to_string(),
549 ),
550 (
551 "user[admin]=1".to_string(),
552 "2-level: user[admin]".to_string(),
553 ),
554 (
555 "profile[admin]=true".to_string(),
556 "2-level: profile[admin]".to_string(),
557 ),
558 (
559 "account[verified]=true".to_string(),
560 "2-level: account[verified]".to_string(),
561 ),
562 (
564 "user[profile][role]=admin".to_string(),
565 "3-level: user[profile][role]".to_string(),
566 ),
567 (
568 "user[permissions][admin]=true".to_string(),
569 "3-level: user[permissions][admin]".to_string(),
570 ),
571 (
572 "account[settings][role]=admin".to_string(),
573 "3-level: account[settings][role]".to_string(),
574 ),
575 (
576 "profile[data][verified]=true".to_string(),
577 "3-level: profile[data][verified]".to_string(),
578 ),
579 (
581 "user[profile][role][level]=admin".to_string(),
582 "4-level: user[profile][role][level]".to_string(),
583 ),
584 (
585 "account[user][permissions][admin]=true".to_string(),
586 "4-level: account[user][permissions][admin]".to_string(),
587 ),
588 (
590 "data[user][profile][permissions][admin]=true".to_string(),
591 "5-level: data[user][profile][permissions][admin]".to_string(),
592 ),
593 (
595 format!("user[{}]=injected", self.test_marker),
596 format!("marker: user[{}]", self.test_marker),
597 ),
598 (
599 format!("user[profile][{}]=injected", self.test_marker),
600 format!("marker: user[profile][{}]", self.test_marker),
601 ),
602 ]
603 }
604
605 fn generate_nested_json_payloads(&self) -> Vec<(Value, String)> {
606 vec![
607 (
609 json!({"user": {"role": "admin"}}),
610 "2-level JSON: user.role".to_string(),
611 ),
612 (
613 json!({"user": {"isAdmin": true}}),
614 "2-level JSON: user.isAdmin".to_string(),
615 ),
616 (
617 json!({"profile": {"admin": true}}),
618 "2-level JSON: profile.admin".to_string(),
619 ),
620 (
622 json!({"user": {"profile": {"role": "admin"}}}),
623 "3-level JSON: user.profile.role".to_string(),
624 ),
625 (
626 json!({"user": {"permissions": {"admin": true}}}),
627 "3-level JSON: user.permissions.admin".to_string(),
628 ),
629 (
630 json!({"account": {"settings": {"verified": true}}}),
631 "3-level JSON: account.settings.verified".to_string(),
632 ),
633 (
635 json!({"user": {"profile": {"permissions": {"admin": true}}}}),
636 "4-level JSON: user.profile.permissions.admin".to_string(),
637 ),
638 (
639 json!({"account": {"user": {"role": {"level": "admin"}}}}),
640 "4-level JSON: account.user.role.level".to_string(),
641 ),
642 (
644 json!({"data": {"user": {"profile": {"permissions": {"admin": true}}}}}),
645 "5-level JSON: data.user.profile.permissions.admin".to_string(),
646 ),
647 (
649 json!({"user": {self.test_marker.clone(): "injected"}}),
650 format!("marker JSON: user.{}", self.test_marker),
651 ),
652 ]
653 }
654
655 async fn test_dot_notation_pollution(
660 &self,
661 url: &str,
662 context: &EndpointContext,
663 ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
664 let mut vulnerabilities = Vec::new();
665
666 info!("[MassAssignment-Advanced] Testing dot notation pollution");
667
668 let dot_payloads = vec![
669 ("user.role=admin", "dot: user.role"),
671 ("user.isAdmin=true", "dot: user.isAdmin"),
672 ("user.admin=1", "dot: user.admin"),
673 ("profile.admin=true", "dot: profile.admin"),
674 ("account.verified=true", "dot: account.verified"),
675 ("user.profile.role=admin", "dot: user.profile.role"),
677 ("user.permissions.admin=true", "dot: user.permissions.admin"),
678 ("account.settings.role=admin", "dot: account.settings.role"),
679 ("profile.data.verified=true", "dot: profile.data.verified"),
680 (
682 "user.profile.permissions.admin=true",
683 "dot: user.profile.permissions.admin",
684 ),
685 (
686 "data.user.account.role=admin",
687 "dot: data.user.account.role",
688 ),
689 ("user.balance=999999", "dot: user.balance"),
691 ("account.credits=999999", "dot: account.credits"),
692 ("subscription.tier=premium", "dot: subscription.tier"),
693 ];
694
695 let tests_run = dot_payloads.len();
696
697 for (payload, technique) in &dot_payloads {
698 let test_url = if url.contains('?') {
699 format!("{}&{}", url, payload)
700 } else {
701 format!("{}?{}", url, payload)
702 };
703
704 match self.http_client.get(&test_url).await {
705 Ok(response) => {
706 if self.verify_dot_notation_injection(&response.body, payload) {
707 info!(
708 "[MassAssignment-Advanced] Dot notation pollution detected: {}",
709 technique
710 );
711 vulnerabilities.push(self.create_vulnerability(
712 url,
713 "Dot Notation Property Pollution",
714 payload,
715 &format!(
716 "Properties can be injected using dot notation syntax. \
717 Technique: {}. Framework: {:?}",
718 technique, context.framework
719 ),
720 &format!("Successfully polluted property using {}", technique),
721 Severity::High,
722 Confidence::High,
723 "CWE-915",
724 8.0,
725 ));
726 return Ok((vulnerabilities, tests_run));
727 }
728 }
729 Err(e) => {
730 debug!("Dot notation test request failed: {}", e);
731 }
732 }
733 }
734
735 let marker_payload = format!("user.{}=injected", self.test_marker);
737 let test_url = if url.contains('?') {
738 format!("{}&{}", url, marker_payload)
739 } else {
740 format!("{}?{}", url, marker_payload)
741 };
742
743 if let Ok(response) = self.http_client.get(&test_url).await {
744 if response
745 .body
746 .to_lowercase()
747 .contains(&self.test_marker.to_lowercase())
748 {
749 vulnerabilities.push(self.create_vulnerability(
750 url,
751 "Dot Notation Property Pollution (Verified)",
752 &marker_payload,
753 "Properties can be injected using dot notation syntax. \
754 Verified with unique marker.",
755 "Successfully polluted property with verified marker injection",
756 Severity::High,
757 Confidence::High,
758 "CWE-915",
759 8.0,
760 ));
761 }
762 }
763
764 Ok((vulnerabilities, tests_run))
765 }
766
767 async fn test_array_parameter_pollution(
772 &self,
773 url: &str,
774 context: &EndpointContext,
775 ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
776 let mut vulnerabilities = Vec::new();
777
778 info!("[MassAssignment-Advanced] Testing array parameter pollution");
779
780 let array_payloads = vec![
781 ("roles[]=admin&roles[]=user", "array: roles[]"),
783 (
784 "permissions[]=admin&permissions[]=read",
785 "array: permissions[]",
786 ),
787 ("groups[]=administrators&groups[]=users", "array: groups[]"),
788 (
790 "permissions[0]=read&permissions[1]=write&permissions[2]=admin",
791 "indexed: permissions[n]",
792 ),
793 ("roles[0]=user&roles[1]=admin", "indexed: roles[n]"),
794 ("users[0][role]=admin", "nested array: users[0][role]"),
796 ("users[0][isAdmin]=true", "nested array: users[0][isAdmin]"),
797 ("items[0][price]=0", "nested array: items[0][price]"),
798 ("orders[0][amount]=0", "nested array: orders[0][amount]"),
799 (
801 "data[users][0][role]=admin",
802 "deep array: data[users][0][role]",
803 ),
804 (
805 "accounts[0][permissions][0]=admin",
806 "deep array: accounts[0][permissions][0]",
807 ),
808 ("users[-1][role]=admin", "negative index: users[-1][role]"),
810 ("items[-1][admin]=true", "negative index: items[-1][admin]"),
811 ("users[9999][role]=admin", "large index: users[9999][role]"),
813 ("role[]=admin", "PHP append: role[]"),
815 ("admin[]=true", "PHP append: admin[]"),
816 ];
817
818 let tests_run = array_payloads.len();
819
820 for (payload, technique) in &array_payloads {
821 let test_url = if url.contains('?') {
822 format!("{}&{}", url, payload)
823 } else {
824 format!("{}?{}", url, payload)
825 };
826
827 match self.http_client.get(&test_url).await {
828 Ok(response) => {
829 if self.verify_array_pollution(&response.body, payload) {
830 info!(
831 "[MassAssignment-Advanced] Array parameter pollution detected: {}",
832 technique
833 );
834 vulnerabilities.push(self.create_vulnerability(
835 url,
836 "Array Parameter Pollution",
837 payload,
838 &format!(
839 "Array parameters can be polluted to inject malicious values. \
840 Technique: {}. Framework: {:?}",
841 technique, context.framework
842 ),
843 &format!("Successfully polluted array using {}", technique),
844 Severity::High,
845 Confidence::Medium,
846 "CWE-915",
847 7.5,
848 ));
849 return Ok((vulnerabilities, tests_run));
850 }
851 }
852 Err(e) => {
853 debug!("Array pollution test request failed: {}", e);
854 }
855 }
856 }
857
858 if context.content_type == ContentType::Json || context.content_type == ContentType::Unknown
860 {
861 let json_array_payloads = vec![
862 (json!({"roles": ["admin", "user"]}), "JSON array: roles"),
863 (
864 json!({"permissions": ["read", "write", "admin"]}),
865 "JSON array: permissions",
866 ),
867 (
868 json!({"user": {"roles": ["admin"]}}),
869 "JSON nested array: user.roles",
870 ),
871 (
872 json!({"users": [{"role": "admin"}]}),
873 "JSON object array: users[].role",
874 ),
875 (
876 json!({"data": {"permissions": ["admin"]}}),
877 "JSON deep array: data.permissions",
878 ),
879 ];
880
881 for (payload, technique) in json_array_payloads {
882 let headers = vec![("Content-Type".to_string(), "application/json".to_string())];
883
884 match self
885 .http_client
886 .post_with_headers(url, &payload.to_string(), headers)
887 .await
888 {
889 Ok(response) => {
890 if self.verify_json_array_pollution(&response.body, &payload) {
891 info!(
892 "[MassAssignment-Advanced] JSON array pollution detected: {}",
893 technique
894 );
895 vulnerabilities.push(self.create_vulnerability(
896 url,
897 "JSON Array Parameter Pollution",
898 &payload.to_string(),
899 &format!(
900 "JSON arrays can be polluted to inject malicious values. \
901 Technique: {}. Framework: {:?}",
902 technique, context.framework
903 ),
904 &format!("Successfully polluted JSON array using {}", technique),
905 Severity::High,
906 Confidence::Medium,
907 "CWE-915",
908 7.5,
909 ));
910 return Ok((vulnerabilities, tests_run));
911 }
912 }
913 Err(e) => {
914 debug!("JSON array pollution test request failed: {}", e);
915 }
916 }
917 }
918 }
919
920 Ok((vulnerabilities, tests_run))
921 }
922
923 async fn test_json_deep_merge(
928 &self,
929 url: &str,
930 context: &EndpointContext,
931 ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
932 let mut vulnerabilities = Vec::new();
933
934 info!("[MassAssignment-Advanced] Testing JSON deep merge exploitation");
935
936 let merge_payloads = vec![
937 (
939 json!({"user": {"role": "admin"}, "profile": {"verified": true}}),
940 "multi-object merge",
941 ),
942 (
943 json!({"__proto__": {"isAdmin": true}}),
944 "prototype pollution merge",
945 ),
946 (
947 json!({"constructor": {"prototype": {"admin": true}}}),
948 "constructor pollution merge",
949 ),
950 (
952 json!({
953 "user": {
954 "profile": {
955 "permissions": {
956 "admin": true,
957 "superuser": true
958 }
959 }
960 }
961 }),
962 "deep 4-level merge",
963 ),
964 (
966 json!({
967 "user": {
968 "roles": ["admin"],
969 "permissions": {"admin": true}
970 }
971 }),
972 "mixed array-object merge",
973 ),
974 (
976 json!({
977 "__proto__": {"isAdmin": true},
978 "user": {"role": "admin"}
979 }),
980 "prototype + object merge",
981 ),
982 (
984 json!({
985 "data": {
986 "nested": {
987 "deep": {
988 "admin": true,
989 "role": "superuser"
990 }
991 }
992 }
993 }),
994 "recursive deep merge",
995 ),
996 (
998 json!({
999 "user": {
1000 self.test_marker.clone(): "injected",
1001 "role": "admin"
1002 }
1003 }),
1004 "verified merge with marker",
1005 ),
1006 ];
1007
1008 let tests_run = merge_payloads.len();
1009
1010 for (payload, technique) in merge_payloads {
1011 let headers = vec![("Content-Type".to_string(), "application/json".to_string())];
1012
1013 match self
1014 .http_client
1015 .post_with_headers(url, &payload.to_string(), headers)
1016 .await
1017 {
1018 Ok(response) => {
1019 if self.verify_deep_merge(&response.body, &payload) {
1020 info!(
1021 "[MassAssignment-Advanced] JSON deep merge exploitation detected: {}",
1022 technique
1023 );
1024 vulnerabilities.push(self.create_vulnerability(
1025 url,
1026 "JSON Deep Merge Exploitation",
1027 &payload.to_string(),
1028 &format!(
1029 "Framework performs deep merge on JSON input allowing property injection. \
1030 Technique: {}. Framework: {:?}",
1031 technique, context.framework
1032 ),
1033 &format!("Successfully exploited deep merge using {}", technique),
1034 Severity::Critical,
1035 Confidence::High,
1036 "CWE-915",
1037 9.0,
1038 ));
1039 return Ok((vulnerabilities, tests_run));
1040 }
1041 }
1042 Err(e) => {
1043 debug!("Deep merge test request failed: {}", e);
1044 }
1045 }
1046 }
1047
1048 Ok((vulnerabilities, tests_run))
1049 }
1050
1051 async fn test_framework_specific(
1056 &self,
1057 url: &str,
1058 context: &EndpointContext,
1059 ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
1060 let mut vulnerabilities = Vec::new();
1061 let mut tests_run = 0;
1062
1063 info!(
1064 "[MassAssignment-Advanced] Testing framework-specific vulnerabilities for {:?}",
1065 context.framework
1066 );
1067
1068 match context.framework {
1069 ApiFramework::Rails => {
1070 let (vulns, tests) = self.test_rails_specific(url).await?;
1071 vulnerabilities.extend(vulns);
1072 tests_run += tests;
1073 }
1074 ApiFramework::Django => {
1075 let (vulns, tests) = self.test_django_specific(url).await?;
1076 vulnerabilities.extend(vulns);
1077 tests_run += tests;
1078 }
1079 ApiFramework::Express => {
1080 let (vulns, tests) = self.test_express_specific(url).await?;
1081 vulnerabilities.extend(vulns);
1082 tests_run += tests;
1083 }
1084 ApiFramework::Spring => {
1085 let (vulns, tests) = self.test_spring_specific(url).await?;
1086 vulnerabilities.extend(vulns);
1087 tests_run += tests;
1088 }
1089 ApiFramework::Laravel => {
1090 let (vulns, tests) = self.test_laravel_specific(url).await?;
1091 vulnerabilities.extend(vulns);
1092 tests_run += tests;
1093 }
1094 _ => {
1095 let (vulns, tests) = self.test_generic_framework(url).await?;
1097 vulnerabilities.extend(vulns);
1098 tests_run += tests;
1099 }
1100 }
1101
1102 Ok((vulnerabilities, tests_run))
1103 }
1104
1105 async fn test_rails_specific(&self, url: &str) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
1107 let mut vulnerabilities = Vec::new();
1108
1109 let rails_payloads = vec![
1110 ("user[admin]=true", "Rails: direct admin"),
1112 (
1113 "user[attributes][admin]=true",
1114 "Rails: nested attributes admin",
1115 ),
1116 (
1117 "user[user_attributes][role]=admin",
1118 "Rails: user_attributes",
1119 ),
1120 (
1121 "model[_destroy]=false&model[admin]=true",
1122 "Rails: _destroy bypass",
1123 ),
1124 ("user[role_ids][]=1", "Rails: has_many through bypass"),
1125 (
1127 "user[profile_attributes][verified]=true",
1128 "Rails: profile_attributes",
1129 ),
1130 (
1131 "order[line_items_attributes][0][price]=0",
1132 "Rails: line_items_attributes",
1133 ),
1134 (
1136 "comment[commentable_type]=Admin&comment[commentable_id]=1",
1137 "Rails: polymorphic abuse",
1138 ),
1139 ];
1140
1141 let tests_run = rails_payloads.len();
1142
1143 for (payload, technique) in &rails_payloads {
1144 let test_url = if url.contains('?') {
1145 format!("{}&{}", url, payload)
1146 } else {
1147 format!("{}?{}", url, payload)
1148 };
1149
1150 if let Ok(response) = self.http_client.get(&test_url).await {
1151 if self.verify_rails_injection(&response.body, payload) {
1152 vulnerabilities.push(self.create_vulnerability(
1153 url,
1154 "Rails Strong Parameters Bypass",
1155 payload,
1156 &format!(
1157 "Rails strong parameters (params.permit) bypassed. Technique: {}",
1158 technique
1159 ),
1160 "Successfully bypassed Rails parameter protection",
1161 Severity::Critical,
1162 Confidence::High,
1163 "CWE-915",
1164 9.0,
1165 ));
1166 return Ok((vulnerabilities, tests_run));
1167 }
1168 }
1169 }
1170
1171 Ok((vulnerabilities, tests_run))
1172 }
1173
1174 async fn test_django_specific(&self, url: &str) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
1176 let mut vulnerabilities = Vec::new();
1177
1178 let django_payloads = vec![
1179 (json!({"is_staff": true}), "DRF: is_staff"),
1181 (json!({"is_superuser": true}), "DRF: is_superuser"),
1182 (json!({"is_active": true}), "DRF: is_active"),
1183 (json!({"groups": [1]}), "DRF: groups assignment"),
1184 (
1185 json!({"user_permissions": [1, 2, 3]}),
1186 "DRF: user_permissions",
1187 ),
1188 (json!({"password": "admin123"}), "DRF: direct password"),
1190 (json!({"pk": 1}), "DRF: primary key manipulation"),
1191 (json!({"id": 1}), "DRF: id manipulation"),
1192 ];
1193
1194 let tests_run = django_payloads.len();
1195 let headers = vec![("Content-Type".to_string(), "application/json".to_string())];
1196
1197 for (payload, technique) in django_payloads {
1198 if let Ok(response) = self
1199 .http_client
1200 .post_with_headers(url, &payload.to_string(), headers.clone())
1201 .await
1202 {
1203 if self.verify_django_injection(&response.body, &payload) {
1204 vulnerabilities.push(self.create_vulnerability(
1205 url,
1206 "Django Serializer Bypass",
1207 &payload.to_string(),
1208 &format!(
1209 "Django REST Framework serializer bypassed. Technique: {}",
1210 technique
1211 ),
1212 "Successfully bypassed Django serializer protection",
1213 Severity::Critical,
1214 Confidence::High,
1215 "CWE-915",
1216 9.0,
1217 ));
1218 return Ok((vulnerabilities, tests_run));
1219 }
1220 }
1221 }
1222
1223 Ok((vulnerabilities, tests_run))
1224 }
1225
1226 async fn test_express_specific(
1228 &self,
1229 url: &str,
1230 ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
1231 let mut vulnerabilities = Vec::new();
1232
1233 let express_payloads = vec![
1234 (
1236 json!({"__proto__": {"isAdmin": true}}),
1237 "Express: __proto__ pollution",
1238 ),
1239 (
1240 json!({"constructor": {"prototype": {"admin": true}}}),
1241 "Express: constructor pollution",
1242 ),
1243 (
1244 json!({"isAdmin": true, "role": "admin"}),
1245 "Express: direct assignment",
1246 ),
1247 (
1249 json!({"$set": {"role": "admin"}}),
1250 "Express/Mongoose: $set injection",
1251 ),
1252 (
1253 json!({"$unset": {"password": ""}}),
1254 "Express/Mongoose: $unset",
1255 ),
1256 (
1258 json!({"$where": "this.admin = true"}),
1259 "Express/Mongoose: $where",
1260 ),
1261 ];
1262
1263 let tests_run = express_payloads.len();
1264 let headers = vec![("Content-Type".to_string(), "application/json".to_string())];
1265
1266 for (payload, technique) in express_payloads {
1267 if let Ok(response) = self
1268 .http_client
1269 .post_with_headers(url, &payload.to_string(), headers.clone())
1270 .await
1271 {
1272 if self.verify_express_injection(&response.body, &payload) {
1273 vulnerabilities.push(self.create_vulnerability(
1274 url,
1275 "Express Body Parser Exploitation",
1276 &payload.to_string(),
1277 &format!(
1278 "Express body parser allows mass assignment. Technique: {}",
1279 technique
1280 ),
1281 "Successfully exploited Express body parser",
1282 Severity::Critical,
1283 Confidence::High,
1284 "CWE-915",
1285 9.0,
1286 ));
1287 return Ok((vulnerabilities, tests_run));
1288 }
1289 }
1290 }
1291
1292 Ok((vulnerabilities, tests_run))
1293 }
1294
1295 async fn test_spring_specific(&self, url: &str) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
1297 let mut vulnerabilities = Vec::new();
1298
1299 let spring_payloads = vec![
1300 (json!({"admin": true}), "Spring: direct admin"),
1302 (
1303 json!({"authorities": [{"authority": "ROLE_ADMIN"}]}),
1304 "Spring: authorities",
1305 ),
1306 (json!({"roles": ["ADMIN"]}), "Spring: roles array"),
1307 (
1308 json!({"enabled": true, "accountNonLocked": true}),
1309 "Spring: account status",
1310 ),
1311 (
1313 json!({"class": {"classLoader": {}}}),
1314 "Spring: class loader injection",
1315 ),
1316 (
1318 json!({"user": {"admin": true}}),
1319 "Spring: nested user.admin",
1320 ),
1321 (
1322 json!({"userDetails": {"authorities": [{"authority": "ADMIN"}]}}),
1323 "Spring: userDetails",
1324 ),
1325 ];
1326
1327 let tests_run = spring_payloads.len();
1328 let headers = vec![("Content-Type".to_string(), "application/json".to_string())];
1329
1330 for (payload, technique) in spring_payloads {
1331 if let Ok(response) = self
1332 .http_client
1333 .post_with_headers(url, &payload.to_string(), headers.clone())
1334 .await
1335 {
1336 if self.verify_spring_injection(&response.body, &payload) {
1337 vulnerabilities.push(self.create_vulnerability(
1338 url,
1339 "Spring Jackson Annotation Bypass",
1340 &payload.to_string(),
1341 &format!(
1342 "Spring Jackson binding allows mass assignment. Technique: {}",
1343 technique
1344 ),
1345 "Successfully bypassed Spring Jackson protection",
1346 Severity::Critical,
1347 Confidence::High,
1348 "CWE-915",
1349 9.0,
1350 ));
1351 return Ok((vulnerabilities, tests_run));
1352 }
1353 }
1354 }
1355
1356 Ok((vulnerabilities, tests_run))
1357 }
1358
1359 async fn test_laravel_specific(
1361 &self,
1362 url: &str,
1363 ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
1364 let mut vulnerabilities = Vec::new();
1365
1366 let laravel_payloads = vec![
1367 (json!({"is_admin": true}), "Laravel: is_admin"),
1369 (json!({"role": "admin"}), "Laravel: role"),
1370 (json!({"role_id": 1}), "Laravel: role_id"),
1371 (
1372 json!({"email_verified_at": "2024-01-01T00:00:00Z"}),
1373 "Laravel: email_verified_at",
1374 ),
1375 (json!({"roles": [{"id": 1}]}), "Laravel: roles relationship"),
1377 (
1378 json!({"permissions": [1, 2, 3]}),
1379 "Laravel: permissions sync",
1380 ),
1381 (
1383 json!({"pivot": {"role_id": 1, "is_admin": true}}),
1384 "Laravel: pivot data",
1385 ),
1386 ];
1387
1388 let tests_run = laravel_payloads.len();
1389 let headers = vec![("Content-Type".to_string(), "application/json".to_string())];
1390
1391 for (payload, technique) in laravel_payloads {
1392 if let Ok(response) = self
1393 .http_client
1394 .post_with_headers(url, &payload.to_string(), headers.clone())
1395 .await
1396 {
1397 if self.verify_laravel_injection(&response.body, &payload) {
1398 vulnerabilities.push(self.create_vulnerability(
1399 url,
1400 "Laravel Eloquent Mass Assignment Bypass",
1401 &payload.to_string(),
1402 &format!(
1403 "Laravel Eloquent $fillable/$guarded bypassed. Technique: {}",
1404 technique
1405 ),
1406 "Successfully bypassed Laravel Eloquent protection",
1407 Severity::Critical,
1408 Confidence::High,
1409 "CWE-915",
1410 9.0,
1411 ));
1412 return Ok((vulnerabilities, tests_run));
1413 }
1414 }
1415 }
1416
1417 Ok((vulnerabilities, tests_run))
1418 }
1419
1420 async fn test_generic_framework(
1422 &self,
1423 url: &str,
1424 ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
1425 let mut vulnerabilities = Vec::new();
1426
1427 let generic_payloads = vec![
1428 (json!({"admin": true}), "generic: admin"),
1429 (json!({"role": "admin"}), "generic: role"),
1430 (json!({"isAdmin": true}), "generic: isAdmin"),
1431 (json!({"verified": true}), "generic: verified"),
1432 (json!({"active": true}), "generic: active"),
1433 ];
1434
1435 let tests_run = generic_payloads.len();
1436 let headers = vec![("Content-Type".to_string(), "application/json".to_string())];
1437
1438 for (payload, technique) in generic_payloads {
1439 if let Ok(response) = self
1440 .http_client
1441 .post_with_headers(url, &payload.to_string(), headers.clone())
1442 .await
1443 {
1444 if self.verify_generic_injection(&response.body, &payload) {
1445 vulnerabilities.push(self.create_vulnerability(
1446 url,
1447 "Mass Assignment Vulnerability",
1448 &payload.to_string(),
1449 &format!(
1450 "Mass assignment vulnerability detected. Technique: {}",
1451 technique
1452 ),
1453 "Successfully exploited mass assignment",
1454 Severity::High,
1455 Confidence::Medium,
1456 "CWE-915",
1457 7.5,
1458 ));
1459 return Ok((vulnerabilities, tests_run));
1460 }
1461 }
1462 }
1463
1464 Ok((vulnerabilities, tests_run))
1465 }
1466
1467 async fn test_privilege_escalation_fields(
1472 &self,
1473 url: &str,
1474 context: &EndpointContext,
1475 ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
1476 let mut vulnerabilities = Vec::new();
1477
1478 info!("[MassAssignment-Advanced] Testing privilege escalation fields");
1479
1480 let privilege_fields = vec![
1481 ("role", "admin"),
1483 ("role", "administrator"),
1484 ("role", "superuser"),
1485 ("userRole", "admin"),
1486 ("user_role", "admin"),
1487 ("isAdmin", "true"),
1489 ("is_admin", "true"),
1490 ("admin", "true"),
1491 ("superuser", "true"),
1492 ("is_superuser", "true"),
1493 ("permissions", "admin"),
1495 ("permission", "all"),
1496 ("access_level", "admin"),
1497 ("accessLevel", "10"),
1498 ("privilege", "admin"),
1499 ("verified", "true"),
1501 ("is_verified", "true"),
1502 ("email_verified", "true"),
1503 ("phone_verified", "true"),
1504 ("account_verified", "true"),
1505 ("active", "true"),
1507 ("is_active", "true"),
1508 ("enabled", "true"),
1509 ("approved", "true"),
1510 ("is_approved", "true"),
1511 ("status", "active"),
1512 ("account_status", "active"),
1513 ];
1514
1515 let tests_run = privilege_fields.len();
1516
1517 for (field, value) in &privilege_fields {
1518 let test_url = if url.contains('?') {
1519 format!("{}&{}={}", url, field, value)
1520 } else {
1521 format!("{}?{}={}", url, field, value)
1522 };
1523
1524 if let Ok(response) = self.http_client.get(&test_url).await {
1525 if self.verify_privilege_escalation(&response.body, field, value) {
1526 vulnerabilities.push(self.create_vulnerability(
1527 url,
1528 "Privilege Escalation via Mass Assignment",
1529 &format!("{}={}", field, value),
1530 &format!(
1531 "User privileges can be escalated by injecting {}={}. Framework: {:?}",
1532 field, value, context.framework
1533 ),
1534 &format!(
1535 "Successfully escalated privileges using {}={}",
1536 field, value
1537 ),
1538 Severity::Critical,
1539 Confidence::High,
1540 "CWE-915",
1541 9.1,
1542 ));
1543 return Ok((vulnerabilities, tests_run));
1544 }
1545 }
1546 }
1547
1548 Ok((vulnerabilities, tests_run))
1549 }
1550
1551 async fn test_ownership_manipulation(
1556 &self,
1557 url: &str,
1558 context: &EndpointContext,
1559 ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
1560 let mut vulnerabilities = Vec::new();
1561
1562 info!("[MassAssignment-Advanced] Testing ownership manipulation");
1563
1564 let ownership_fields = vec![
1565 ("id", "1"),
1566 ("user_id", "1"),
1567 ("userId", "1"),
1568 ("owner_id", "1"),
1569 ("ownerId", "1"),
1570 ("account_id", "1"),
1571 ("accountId", "1"),
1572 ("org_id", "1"),
1573 ("organization_id", "1"),
1574 ("tenant_id", "1"),
1575 ("parent_id", "1"),
1576 ("created_by", "1"),
1577 ("createdBy", "1"),
1578 ("author_id", "1"),
1579 ];
1580
1581 let tests_run = ownership_fields.len();
1582
1583 for (field, value) in &ownership_fields {
1584 let test_url = if url.contains('?') {
1585 format!("{}&{}={}", url, field, value)
1586 } else {
1587 format!("{}?{}={}", url, field, value)
1588 };
1589
1590 if let Ok(response) = self.http_client.get(&test_url).await {
1591 if self.verify_ownership_change(&response.body, field, value) {
1592 vulnerabilities.push(self.create_vulnerability(
1593 url,
1594 "Ownership Manipulation via Mass Assignment",
1595 &format!("{}={}", field, value),
1596 &format!(
1597 "Object ownership can be changed by injecting {}={}. \
1598 This may lead to IDOR or privilege escalation. Framework: {:?}",
1599 field, value, context.framework
1600 ),
1601 &format!(
1602 "Successfully manipulated ownership using {}={}",
1603 field, value
1604 ),
1605 Severity::High,
1606 Confidence::Medium,
1607 "CWE-915",
1608 8.0,
1609 ));
1610 return Ok((vulnerabilities, tests_run));
1611 }
1612 }
1613 }
1614
1615 Ok((vulnerabilities, tests_run))
1616 }
1617
1618 async fn test_financial_field_manipulation(
1623 &self,
1624 url: &str,
1625 context: &EndpointContext,
1626 ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
1627 let mut vulnerabilities = Vec::new();
1628
1629 info!("[MassAssignment-Advanced] Testing financial field manipulation");
1630
1631 let financial_fields = vec![
1632 ("balance", "999999"),
1634 ("credits", "999999"),
1635 ("points", "999999"),
1636 ("coins", "999999"),
1637 ("price", "0"),
1639 ("amount", "0"),
1640 ("total", "0"),
1641 ("subtotal", "0"),
1642 ("cost", "0"),
1643 ("fee", "0"),
1644 ("discount", "100"),
1645 ("discount_percent", "100"),
1646 ("subscription", "premium"),
1648 ("tier", "enterprise"),
1649 ("plan", "unlimited"),
1650 ("subscription_type", "lifetime"),
1651 ("is_premium", "true"),
1652 ("is_pro", "true"),
1653 ("limit", "999999"),
1655 ("quota", "999999"),
1656 ("max_items", "999999"),
1657 ("rate_limit", "999999"),
1658 ];
1659
1660 let tests_run = financial_fields.len();
1661
1662 for (field, value) in &financial_fields {
1663 let test_url = if url.contains('?') {
1664 format!("{}&{}={}", url, field, value)
1665 } else {
1666 format!("{}?{}={}", url, field, value)
1667 };
1668
1669 if let Ok(response) = self.http_client.get(&test_url).await {
1670 if self.verify_financial_manipulation(&response.body, field, value) {
1671 let severity = if field.contains("price")
1672 || field.contains("amount")
1673 || field.contains("balance")
1674 {
1675 Severity::Critical
1676 } else {
1677 Severity::High
1678 };
1679
1680 vulnerabilities.push(self.create_vulnerability(
1681 url,
1682 "Financial Field Manipulation via Mass Assignment",
1683 &format!("{}={}", field, value),
1684 &format!(
1685 "Financial/business fields can be manipulated by injecting {}={}. \
1686 This may lead to financial fraud. Framework: {:?}",
1687 field, value, context.framework
1688 ),
1689 &format!(
1690 "Successfully manipulated financial field {}={}",
1691 field, value
1692 ),
1693 severity,
1694 Confidence::High,
1695 "CWE-915",
1696 9.0,
1697 ));
1698 return Ok((vulnerabilities, tests_run));
1699 }
1700 }
1701 }
1702
1703 Ok((vulnerabilities, tests_run))
1704 }
1705
1706 async fn test_timestamp_manipulation(
1711 &self,
1712 url: &str,
1713 context: &EndpointContext,
1714 ) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
1715 let mut vulnerabilities = Vec::new();
1716
1717 info!("[MassAssignment-Advanced] Testing timestamp manipulation");
1718
1719 let timestamp_fields = vec![
1720 ("created_at", "2020-01-01T00:00:00Z"),
1721 ("createdAt", "2020-01-01T00:00:00Z"),
1722 ("updated_at", "2030-01-01T00:00:00Z"),
1723 ("updatedAt", "2030-01-01T00:00:00Z"),
1724 ("deleted_at", "null"),
1725 ("deletedAt", "null"),
1726 ("expires_at", "2030-01-01T00:00:00Z"),
1727 ("expiresAt", "2030-01-01T00:00:00Z"),
1728 ("valid_until", "2030-01-01T00:00:00Z"),
1729 ("subscription_ends", "2030-01-01T00:00:00Z"),
1730 ("trial_ends_at", "2030-01-01T00:00:00Z"),
1731 ("last_login", "2020-01-01T00:00:00Z"),
1732 ("password_changed_at", "2030-01-01T00:00:00Z"),
1733 ];
1734
1735 let tests_run = timestamp_fields.len();
1736
1737 for (field, value) in ×tamp_fields {
1738 let test_url = if url.contains('?') {
1739 format!("{}&{}={}", url, field, value)
1740 } else {
1741 format!("{}?{}={}", url, field, value)
1742 };
1743
1744 if let Ok(response) = self.http_client.get(&test_url).await {
1745 if self.verify_timestamp_manipulation(&response.body, field, value) {
1746 vulnerabilities.push(self.create_vulnerability(
1747 url,
1748 "Timestamp Manipulation via Mass Assignment",
1749 &format!("{}={}", field, value),
1750 &format!(
1751 "Timestamp fields can be manipulated by injecting {}={}. \
1752 This may bypass time-based restrictions. Framework: {:?}",
1753 field, value, context.framework
1754 ),
1755 &format!("Successfully manipulated timestamp {}={}", field, value),
1756 Severity::Medium,
1757 Confidence::Medium,
1758 "CWE-915",
1759 6.0,
1760 ));
1761 return Ok((vulnerabilities, tests_run));
1762 }
1763 }
1764 }
1765
1766 Ok((vulnerabilities, tests_run))
1767 }
1768
1769 fn verify_nested_injection(&self, body: &str, payload: &str) -> bool {
1774 if !self.is_structured_response(body) {
1775 return false;
1776 }
1777
1778 let body_lower = body.to_lowercase();
1779
1780 if body_lower.contains(&self.test_marker.to_lowercase()) {
1782 return true;
1783 }
1784
1785 if payload.contains("role]=admin") || payload.contains("role=admin") {
1787 if body_lower.contains("\"role\":\"admin\"")
1788 || body_lower.contains("\"role\": \"admin\"")
1789 {
1790 return true;
1791 }
1792 }
1793
1794 if payload.contains("admin]=true")
1795 || payload.contains("admin=true")
1796 || payload.contains("admin]=1")
1797 {
1798 if body_lower.contains("\"admin\":true")
1799 || body_lower.contains("\"admin\": true")
1800 || body_lower.contains("\"admin\":1")
1801 {
1802 return true;
1803 }
1804 }
1805
1806 if payload.contains("isadmin]=true") || payload.contains("isadmin=true") {
1807 if body_lower.contains("\"isadmin\":true") || body_lower.contains("\"isadmin\": true") {
1808 return true;
1809 }
1810 }
1811
1812 false
1813 }
1814
1815 fn verify_json_injection(&self, body: &str, payload: &Value) -> bool {
1816 if !self.is_structured_response(body) {
1817 return false;
1818 }
1819
1820 let body_lower = body.to_lowercase();
1821
1822 if body_lower.contains(&self.test_marker.to_lowercase()) {
1824 return true;
1825 }
1826
1827 if let Ok(response_json) = serde_json::from_str::<Value>(body) {
1829 return self.json_contains_injected_value(&response_json, payload);
1830 }
1831
1832 false
1833 }
1834
1835 fn json_contains_injected_value(&self, response: &Value, payload: &Value) -> bool {
1836 if let Some(obj) = payload.as_object() {
1838 for (_key, value) in obj {
1839 if let Some(inner_obj) = value.as_object() {
1840 for (inner_key, inner_value) in inner_obj {
1841 if inner_key == "role" && inner_value == "admin" {
1842 if self.json_has_value(response, "role", "admin") {
1843 return true;
1844 }
1845 }
1846 if inner_key == "admin" || inner_key == "isAdmin" {
1847 if inner_value == true {
1848 if self.json_has_bool(response, inner_key, true) {
1849 return true;
1850 }
1851 }
1852 }
1853 }
1854 }
1855 }
1856 }
1857
1858 false
1859 }
1860
1861 fn json_has_value(&self, json: &Value, key: &str, value: &str) -> bool {
1862 match json {
1863 Value::Object(map) => {
1864 if let Some(v) = map.get(key) {
1865 if v.as_str() == Some(value) {
1866 return true;
1867 }
1868 }
1869 for (_, v) in map {
1870 if self.json_has_value(v, key, value) {
1871 return true;
1872 }
1873 }
1874 }
1875 Value::Array(arr) => {
1876 for item in arr {
1877 if self.json_has_value(item, key, value) {
1878 return true;
1879 }
1880 }
1881 }
1882 _ => {}
1883 }
1884 false
1885 }
1886
1887 fn json_has_bool(&self, json: &Value, key: &str, value: bool) -> bool {
1888 match json {
1889 Value::Object(map) => {
1890 if let Some(v) = map.get(key) {
1891 if v.as_bool() == Some(value) {
1892 return true;
1893 }
1894 }
1895 for (_, v) in map {
1896 if self.json_has_bool(v, key, value) {
1897 return true;
1898 }
1899 }
1900 }
1901 Value::Array(arr) => {
1902 for item in arr {
1903 if self.json_has_bool(item, key, value) {
1904 return true;
1905 }
1906 }
1907 }
1908 _ => {}
1909 }
1910 false
1911 }
1912
1913 fn verify_dot_notation_injection(&self, body: &str, payload: &str) -> bool {
1914 self.verify_nested_injection(body, payload)
1915 }
1916
1917 fn verify_array_pollution(&self, body: &str, payload: &str) -> bool {
1918 if !self.is_structured_response(body) {
1919 return false;
1920 }
1921
1922 let body_lower = body.to_lowercase();
1923
1924 if body_lower.contains(&self.test_marker.to_lowercase()) {
1926 return true;
1927 }
1928
1929 if payload.contains("admin") && body.contains('[') {
1931 if body_lower.contains("\"admin\":true") || body_lower.contains("\"role\":\"admin\"") {
1932 return true;
1933 }
1934 }
1935
1936 if payload.contains("price]=0") || payload.contains("amount]=0") {
1938 if body_lower.contains("\"price\":0") || body_lower.contains("\"amount\":0") {
1939 return true;
1940 }
1941 }
1942
1943 false
1944 }
1945
1946 fn verify_json_array_pollution(&self, body: &str, payload: &Value) -> bool {
1947 if !self.is_structured_response(body) {
1948 return false;
1949 }
1950
1951 let body_lower = body.to_lowercase();
1952
1953 if body_lower.contains("\"roles\"") && (body_lower.contains("\"admin\"") || body_lower.contains(":\"admin\"")) {
1956 return true;
1957 }
1958
1959 if body_lower.contains("\"permissions\"") && (body_lower.contains("\"admin\"") || body_lower.contains(":\"admin\"")) {
1961 return true;
1962 }
1963
1964 false
1965 }
1966
1967 fn verify_deep_merge(&self, body: &str, payload: &Value) -> bool {
1968 if !self.is_structured_response(body) {
1969 return false;
1970 }
1971
1972 let body_lower = body.to_lowercase();
1973
1974 if body_lower.contains(&self.test_marker.to_lowercase()) {
1976 return true;
1977 }
1978
1979 if body_lower.contains("\"__proto__\"") && (body_lower.contains("\"admin\":true") || body_lower.contains("\"role\":\"admin\"")) {
1981 return true;
1982 }
1983
1984 self.verify_json_injection(body, payload)
1986 }
1987
1988 fn verify_rails_injection(&self, body: &str, _payload: &str) -> bool {
1989 if !self.is_structured_response(body) {
1990 return false;
1991 }
1992
1993 let body_lower = body.to_lowercase();
1994
1995 body_lower.contains("\"admin\":true")
1996 || body_lower.contains("\"role\":\"admin\"")
1997 || body_lower.contains("\"verified\":true")
1998 }
1999
2000 fn verify_django_injection(&self, body: &str, payload: &Value) -> bool {
2001 if !self.is_structured_response(body) {
2002 return false;
2003 }
2004
2005 let body_lower = body.to_lowercase();
2006
2007 if body_lower.contains("\"is_staff\":true") || body_lower.contains("\"is_superuser\":true")
2009 {
2010 return true;
2011 }
2012
2013 self.verify_json_injection(body, payload)
2014 }
2015
2016 fn verify_express_injection(&self, body: &str, payload: &Value) -> bool {
2017 if !self.is_structured_response(body) {
2018 return false;
2019 }
2020
2021 let body_lower = body.to_lowercase();
2022
2023 if body_lower.contains("\"__proto__\"") || body_lower.contains("\"constructor\"") {
2025 if body_lower.contains("\"admin\":true") || body_lower.contains("\"role\":\"admin\"") {
2026 return true;
2027 }
2028 }
2029
2030 self.verify_json_injection(body, payload)
2031 }
2032
2033 fn verify_spring_injection(&self, body: &str, payload: &Value) -> bool {
2034 if !self.is_structured_response(body) {
2035 return false;
2036 }
2037
2038 let body_lower = body.to_lowercase();
2039
2040 if body_lower.contains("\"authorities\"") && (body_lower.contains("\"admin\"") || body_lower.contains(":\"admin\"")) {
2042 return true;
2043 }
2044
2045 self.verify_json_injection(body, payload)
2046 }
2047
2048 fn verify_laravel_injection(&self, body: &str, payload: &Value) -> bool {
2049 if !self.is_structured_response(body) {
2050 return false;
2051 }
2052
2053 let body_lower = body.to_lowercase();
2054
2055 if body_lower.contains("\"is_admin\":true") || body_lower.contains("\"role\":\"admin\"") {
2057 return true;
2058 }
2059
2060 if body_lower.contains("\"email_verified_at\"") && !body_lower.contains("null") {
2061 return true;
2062 }
2063
2064 self.verify_json_injection(body, payload)
2065 }
2066
2067 fn verify_generic_injection(&self, body: &str, payload: &Value) -> bool {
2068 self.verify_json_injection(body, payload)
2069 }
2070
2071 fn verify_privilege_escalation(&self, body: &str, field: &str, value: &str) -> bool {
2072 if !self.is_structured_response(body) {
2073 return false;
2074 }
2075
2076 let body_lower = body.to_lowercase();
2077 let field_lower = field.to_lowercase();
2078 let value_lower = value.to_lowercase();
2079
2080 let patterns = vec![
2082 format!("\"{}\":\"{}\"", field_lower, value_lower),
2083 format!("\"{}\": \"{}\"", field_lower, value_lower),
2084 format!("\"{}\":{}", field_lower, value_lower),
2085 format!("\"{}\": {}", field_lower, value_lower),
2086 ];
2087
2088 for pattern in patterns {
2089 if body_lower.contains(&pattern) {
2090 return true;
2091 }
2092 }
2093
2094 false
2095 }
2096
2097 fn verify_ownership_change(&self, body: &str, field: &str, value: &str) -> bool {
2098 self.verify_privilege_escalation(body, field, value)
2099 }
2100
2101 fn verify_financial_manipulation(&self, body: &str, field: &str, value: &str) -> bool {
2102 self.verify_privilege_escalation(body, field, value)
2103 }
2104
2105 fn verify_timestamp_manipulation(&self, body: &str, field: &str, value: &str) -> bool {
2106 if !self.is_structured_response(body) {
2107 return false;
2108 }
2109
2110 let body_lower = body.to_lowercase();
2111 let field_lower = field.to_lowercase();
2112
2113 if body_lower.contains(&format!("\"{}\":", field_lower)) {
2115 if body_lower.contains("2020-01-01") || body_lower.contains("2030-01-01") {
2117 return true;
2118 }
2119 }
2120
2121 false
2122 }
2123
2124 fn is_structured_response(&self, body: &str) -> bool {
2125 let trimmed = body.trim();
2126 trimmed.starts_with('{') || trimmed.starts_with('[')
2127 }
2128
2129 fn create_vulnerability(
2134 &self,
2135 url: &str,
2136 vuln_type: &str,
2137 payload: &str,
2138 description: &str,
2139 evidence: &str,
2140 severity: Severity,
2141 confidence: Confidence,
2142 cwe: &str,
2143 cvss: f64,
2144 ) -> Vulnerability {
2145 Vulnerability {
2146 id: format!("maa_{}", generate_uuid()),
2147 vuln_type: vuln_type.to_string(),
2148 severity,
2149 confidence,
2150 category: "Business Logic".to_string(),
2151 url: url.to_string(),
2152 parameter: None,
2153 payload: payload.to_string(),
2154 description: description.to_string(),
2155 evidence: Some(evidence.to_string()),
2156 cwe: cwe.to_string(),
2157 cvss: cvss as f32,
2158 verified: true,
2159 false_positive: false,
2160 remediation: self.get_remediation(),
2161 discovered_at: chrono::Utc::now().to_rfc3339(),
2162 ml_confidence: None,
2163 ml_data: None,
2164 }
2165 }
2166
2167 fn get_remediation(&self) -> String {
2168 r#"MASS ASSIGNMENT REMEDIATION:
2169===============================
2170
21711. USE ALLOWLISTS (RECOMMENDED):
2172 - Rails: Use strong parameters with permit()
2173 - Django: Specify fields in serializer's Meta class
2174 - Express: Validate and pick only allowed fields
2175 - Spring: Use @JsonIgnore on sensitive fields
2176 - Laravel: Use $fillable array in Eloquent models
2177
21782. AVOID BLOCKLISTS:
2179 - Blocklists ($guarded) are error-prone
2180 - Always prefer explicit allowlists
2181
21823. USE DATA TRANSFER OBJECTS (DTOs):
2183 - Create separate input/output models
2184 - Never bind directly to domain models
2185
21864. VALIDATE INPUT STRICTLY:
2187 - Validate all input against expected schema
2188 - Reject unexpected fields
2189
21905. PROTECT SENSITIVE FIELDS:
2191 - Mark role, admin, verified as read-only
2192 - Implement field-level access controls
2193
21946. BLOCK DANGEROUS PATTERNS:
2195 - Reject __proto__, constructor, prototype
2196 - Limit nested object depth
2197 - Sanitize array indices
2198
21997. IMPLEMENT AUTHORIZATION:
2200 - Check permissions before updates
2201 - Use separate endpoints for admin operations
2202
22038. FRAMEWORK-SPECIFIC:
2204 Rails: params.require(:user).permit(:name, :email)
2205 Django: class UserSerializer(serializers.Serializer):
2206 class Meta:
2207 fields = ['name', 'email']
2208 Express: const { name, email } = req.body; // Only pick allowed
2209 Spring: @JsonIgnoreProperties(value = {"admin", "role"})
2210 Laravel: protected $fillable = ['name', 'email'];
2211
22129. USE OBJECT.CREATE(NULL):
2213 - For JavaScript, prevent prototype pollution
2214 - const obj = Object.create(null);
2215
221610. IMPLEMENT STRICT JSON SCHEMA:
2217 - Validate all nested objects
2218 - Limit depth and array sizes"#
2219 .to_string()
2220 }
2221}
2222
2223fn generate_uuid() -> String {
2225 use rand::RngExt;
2226 let mut rng = rand::rng();
2227 format!(
2228 "{:08x}{:04x}{:04x}{:04x}{:012x}",
2229 rng.random::<u32>(),
2230 rng.random::<u16>(),
2231 rng.random::<u16>(),
2232 rng.random::<u16>(),
2233 rng.random::<u64>() & 0xffffffffffff
2234 )
2235}
2236
2237#[cfg(test)]
2238mod tests {
2239 use super::*;
2240 use crate::http_client::HttpClient;
2241 use std::sync::Arc;
2242
2243 fn create_test_scanner() -> AdvancedMassAssignmentScanner {
2244 let http_client = Arc::new(HttpClient::new(30, 3).unwrap());
2245 AdvancedMassAssignmentScanner::new(http_client)
2246 }
2247
2248 #[test]
2249 fn test_unique_marker_generation() {
2250 let scanner1 = create_test_scanner();
2251 let scanner2 = create_test_scanner();
2252
2253 assert_ne!(scanner1.test_marker, scanner2.test_marker);
2254 assert!(scanner1.test_marker.starts_with("maa_"));
2255 }
2256
2257 #[test]
2258 fn test_framework_detection() {
2259 let scanner = create_test_scanner();
2260
2261 let mut headers = std::collections::HashMap::new();
2263 headers.insert("server".to_string(), "Passenger".to_string());
2264 let framework = scanner.detect_framework(&headers, "");
2265 assert_eq!(framework, ApiFramework::Rails);
2266
2267 let mut headers = std::collections::HashMap::new();
2269 headers.insert("x-powered-by".to_string(), "Express".to_string());
2270 let framework = scanner.detect_framework(&headers, "");
2271 assert_eq!(framework, ApiFramework::Express);
2272
2273 let mut headers = std::collections::HashMap::new();
2275 headers.insert("server".to_string(), "gunicorn".to_string());
2276 let framework = scanner.detect_framework(&headers, "Django");
2277 assert_eq!(framework, ApiFramework::Django);
2278 }
2279
2280 #[test]
2281 fn test_nested_injection_detection() {
2282 let scanner = create_test_scanner();
2283
2284 let body_with_marker = format!(r#"{{"user":{{"{}":"injected"}}}}"#, scanner.test_marker);
2286 assert!(scanner.verify_nested_injection(
2287 &body_with_marker,
2288 &format!("user[{}]=injected", scanner.test_marker)
2289 ));
2290
2291 assert!(scanner.verify_nested_injection(r#"{"user":{"role":"admin"}}"#, "user[role]=admin"));
2293
2294 assert!(scanner.verify_nested_injection(r#"{"user":{"admin":true}}"#, "user[admin]=true"));
2296
2297 assert!(scanner
2299 .verify_nested_injection(r#"{"profile":{"isAdmin":true}}"#, "profile[isAdmin]=true"));
2300 }
2301
2302 #[test]
2303 fn test_no_false_positives() {
2304 let scanner = create_test_scanner();
2305
2306 assert!(!scanner
2308 .verify_nested_injection("<html><body>admin panel</body></html>", "user[role]=admin"));
2309
2310 assert!(!scanner.verify_nested_injection("Welcome admin user", "user[admin]=true"));
2312
2313 assert!(!scanner.verify_nested_injection(r#"{"message":"success"}"#, "user[role]=admin"));
2315 }
2316
2317 #[test]
2318 fn test_privilege_escalation_verification() {
2319 let scanner = create_test_scanner();
2320
2321 assert!(scanner.verify_privilege_escalation(r#"{"role":"admin"}"#, "role", "admin"));
2322
2323 assert!(scanner.verify_privilege_escalation(r#"{"isAdmin":true}"#, "isAdmin", "true"));
2324
2325 assert!(!scanner.verify_privilege_escalation(r#"{"role":"user"}"#, "role", "admin"));
2326 }
2327
2328 #[test]
2329 fn test_array_pollution_verification() {
2330 let scanner = create_test_scanner();
2331
2332 assert!(scanner.verify_array_pollution(r#"[{"admin":true}]"#, "users[0][admin]=true"));
2333
2334 assert!(scanner.verify_array_pollution(r#"[{"role":"admin"}]"#, "users[0][role]=admin"));
2335
2336 assert!(!scanner.verify_array_pollution(r#"{"message":"ok"}"#, "users[0][admin]=true"));
2337 }
2338
2339 #[test]
2340 fn test_data_modification_endpoint_detection() {
2341 let scanner = create_test_scanner();
2342
2343 assert!(scanner.is_data_modification_endpoint("/api/users/create"));
2344 assert!(scanner.is_data_modification_endpoint("/api/profile/update"));
2345 assert!(scanner.is_data_modification_endpoint("/api/settings"));
2346 assert!(scanner.is_data_modification_endpoint("/api/account/register"));
2347
2348 assert!(!scanner.is_data_modification_endpoint("/api/users/list"));
2349 assert!(!scanner.is_data_modification_endpoint("/static/image.png"));
2350 }
2351
2352 #[test]
2353 fn test_json_field_extraction() {
2354 let scanner = create_test_scanner();
2355
2356 let json = json!({
2357 "user": {
2358 "name": "test",
2359 "profile": {
2360 "role": "user"
2361 }
2362 }
2363 });
2364
2365 let fields = scanner.extract_field_names(&json);
2366
2367 assert!(fields.contains("user"));
2368 assert!(fields.contains("name"));
2369 assert!(fields.contains("profile"));
2370 assert!(fields.contains("role"));
2371 assert!(fields.contains("user.name"));
2372 assert!(fields.contains("user.profile"));
2373 assert!(fields.contains("user.profile.role"));
2374 }
2375
2376 #[test]
2377 fn test_vulnerability_creation() {
2378 let scanner = create_test_scanner();
2379
2380 let vuln = scanner.create_vulnerability(
2381 "https://example.com/api/users",
2382 "Mass Assignment Test",
2383 "role=admin",
2384 "Test description",
2385 "Test evidence",
2386 Severity::Critical,
2387 Confidence::High,
2388 "CWE-915",
2389 9.0,
2390 );
2391
2392 assert!(vuln.id.starts_with("maa_"));
2393 assert_eq!(vuln.severity, Severity::Critical);
2394 assert_eq!(vuln.confidence, Confidence::High);
2395 assert_eq!(vuln.cwe, "CWE-915");
2396 assert_eq!(vuln.cvss, 9.0);
2397 assert!(vuln.verified);
2398 }
2399
2400 #[test]
2401 fn test_nested_url_payload_generation() {
2402 let scanner = create_test_scanner();
2403
2404 let payloads = scanner.generate_nested_url_payloads();
2405
2406 assert!(!payloads.is_empty());
2407
2408 let has_2_level = payloads.iter().any(|(p, _)| p.contains("user[role]=admin"));
2410 let has_3_level = payloads
2411 .iter()
2412 .any(|(p, _)| p.contains("user[profile][role]=admin"));
2413 let has_4_level = payloads
2414 .iter()
2415 .any(|(p, _)| p.contains("user[profile][role][level]=admin"));
2416
2417 assert!(has_2_level);
2418 assert!(has_3_level);
2419 assert!(has_4_level);
2420
2421 let has_marker = payloads
2423 .iter()
2424 .any(|(p, _)| p.contains(&scanner.test_marker));
2425 assert!(has_marker);
2426 }
2427
2428 #[test]
2429 fn test_deep_merge_verification() {
2430 let scanner = create_test_scanner();
2431
2432 let payload = json!({"user": {"role": "admin"}});
2433
2434 assert!(scanner.verify_deep_merge(r#"{"user":{"role":"admin"}}"#, &payload));
2435
2436 let proto_payload = json!({"__proto__": {"admin": true}});
2438 assert!(scanner.verify_deep_merge(r#"{"__proto__":{"admin":true}}"#, &proto_payload));
2439 }
2440}