השמה המונית - Mass Assignment¶
מהי השמה המונית¶
השמה המונית (Mass Assignment) היא חולשה שמתרחשת כאשר אפליקציה מקשרת אוטומטית פרמטרים מבקשת HTTP לשדות במודל הנתונים, ללא סינון. התוקף מוסיף פרמטרים נוספים לבקשה - פרמטרים שלא אמורים להיות ניתנים לשליטת המשתמש - ומצליח לשנות שדות פנימיים כמו הרשאות, יתרות, או סטטוס אימות.
החולשה מוכרת בשמות נוספים בהתאם למסגרת העבודה:
- Ruby on Rails - Mass Assignment
- ASP.NET - Over-Posting
- PHP/Laravel - Mass Assignment
- Node.js - Object Injection / Prototype Pollution
- שם כללי - Auto-Binding
הבנת הבעיה¶
בקשת הרשמה תקינה:
POST /register HTTP/1.1
Content-Type: application/json
{
"username": "newuser",
"email": "user@example.com",
"password": "securePass123"
}
בקשת הרשמה זדונית:
POST /register HTTP/1.1
Content-Type: application/json
{
"username": "newuser",
"email": "user@example.com",
"password": "securePass123",
"role": "admin",
"isVerified": true,
"balance": 999999
}
אם האפליקציה מקשרת את כל הפרמטרים אוטומטית, השדות role, isVerified, ו-balance יעודכנו גם הם.
השמה המונית ב-Ruby on Rails¶
הבעיה¶
# קוד פגיע - Rails < 4.0
class User < ActiveRecord::Base
# כל השדות ניתנים לעדכון
end
class UsersController < ApplicationController
def create
# פגיע - כל הפרמטרים מועברים ישירות
@user = User.new(params[:user])
@user.save
end
end
המודל במסד הנתונים:
# migration
create_table :users do |t|
t.string :username
t.string :email
t.string :password_digest
t.string :role, default: 'user'
t.boolean :is_admin, default: false
t.decimal :balance, default: 0
t.timestamps
end
ההגנה - Strong Parameters (Rails >= 4.0)¶
class UsersController < ApplicationController
def create
# מוגן - רק פרמטרים מורשים
@user = User.new(user_params)
@user.save
end
private
def user_params
params.require(:user).permit(:username, :email, :password)
# role, is_admin, balance - לא מורשים!
end
end
ההגנה הישנה - attr_accessible (Rails < 4.0)¶
class User < ActiveRecord::Base
# רק שדות אלה ניתנים להשמה המונית
attr_accessible :username, :email, :password
# או להפך - חסימת שדות רגישים
attr_protected :role, :is_admin, :balance
end
השמה המונית ב-Django¶
הבעיה¶
# models.py
class User(models.Model):
username = models.CharField(max_length=100)
email = models.EmailField()
password = models.CharField(max_length=128)
role = models.CharField(max_length=20, default='user')
is_staff = models.BooleanField(default=False)
balance = models.DecimalField(default=0)
# views.py - פגיע
def register(request):
if request.method == 'POST':
# פגיע - כל השדות מועברים
user = User(**request.POST.dict())
user.save()
return redirect('/dashboard')
ההגנה - ModelForm עם fields¶
# forms.py - מוגן
class UserRegistrationForm(forms.ModelForm):
class Meta:
model = User
fields = ['username', 'email', 'password']
# רק שדות אלה מותרים
# לא להשתמש ב-exclude - עדיף fields!
# exclude = ['role', 'is_staff'] # מסוכן - שדות חדשים לא יחסמו
# views.py - מוגן
def register(request):
if request.method == 'POST':
form = UserRegistrationForm(request.POST)
if form.is_valid():
user = form.save()
return redirect('/dashboard')
השמה המונית ב-Express.js (Node.js)¶
הבעיה עם Mongoose¶
// models/User.js
const userSchema = new mongoose.Schema({
username: String,
email: String,
password: String,
role: { type: String, default: 'user' },
isAdmin: { type: Boolean, default: false },
balance: { type: Number, default: 0 },
apiKey: String,
resetToken: String
});
// routes/users.js - פגיע
app.post('/register', async (req, res) => {
// פגיע - כל הפרמטרים מועברים ישירות למודל
const user = new User(req.body);
await user.save();
res.json({ success: true });
});
// פגיע גם בעדכון
app.put('/profile', async (req, res) => {
// פגיע - מעדכן כל שדה שנשלח
await User.findByIdAndUpdate(req.session.userId, req.body);
res.json({ success: true });
});
ההגנה - סינון שדות מפורש¶
// routes/users.js - מוגן
app.post('/register', async (req, res) => {
// מוגן - רק שדות מורשים
const { username, email, password } = req.body;
const user = new User({ username, email, password });
await user.save();
res.json({ success: true });
});
// עדכון פרופיל מוגן
app.put('/profile', async (req, res) => {
const allowedFields = ['username', 'email', 'avatar'];
const updates = {};
for (const field of allowedFields) {
if (req.body[field] !== undefined) {
updates[field] = req.body[field];
}
}
await User.findByIdAndUpdate(req.session.userId, updates);
res.json({ success: true });
});
השמה המונית ב-Laravel (PHP)¶
הבעיה¶
// app/Models/User.php
class User extends Model {
// פגיע - אין הגבלה על שדות
protected $guarded = [];
// או:
// ללא הגדרת $fillable כלל
}
// app/Http/Controllers/UserController.php - פגיע
public function store(Request $request) {
// פגיע - כל הפרמטרים מועברים
$user = User::create($request->all());
return response()->json($user);
}
ההגנה - fillable ו-guarded¶
// app/Models/User.php - מוגן
class User extends Model {
// גישה 1: רשימה לבנה - רק שדות אלה ניתנים להשמה
protected $fillable = ['username', 'email', 'password'];
// גישה 2: רשימה שחורה - שדות אלה חסומים
// protected $guarded = ['role', 'is_admin', 'balance'];
// פחות מומלץ - שדות חדשים לא יחסמו אוטומטית
// שדות נסתרים (לא מוחזרים ב-JSON)
protected $hidden = ['password', 'api_key', 'reset_token'];
}
גילוי פרמטרים נסתרים¶
קריאת קוד JavaScript בצד הלקוח¶
// לעיתים הקוד בצד הלקוח חושף את מבנה המודל
// חפשו בקבצי JS:
// דוגמה 1: אובייקט משתמש בקוד
const user = {
username: "",
email: "",
password: "",
role: "user", // שדה פנימי!
isAdmin: false, // שדה פנימי!
accountType: "free" // שדה פנימי!
};
// דוגמה 2: ולידציה בצד לקוח שחושפת שדות
function validateForm(data) {
if (data.role && !['user', 'admin'].includes(data.role)) {
// חושף את הערכים המותרים של role
}
}
קריאת תיעוד API¶
הודעות שגיאה מפורטות¶
POST /register HTTP/1.1
Content-Type: application/json
{"username": "test", "email": "test@test.com", "password": "123", "foobar": "test"}
HTTP/1.1 422 Unprocessable Entity
{
"errors": {
"foobar": "Unknown field. Valid fields: username, email, password, role, verified, tier"
}
}
הודעת השגיאה חושפת את כל השדות התקפים, כולל הפנימיים.
כלי Param Miner של Burp¶
Param Miner מנסה אוטומטית פרמטרים נפוצים:
Param Miner Settings:
- wordlist: common parameter names
- method: add to body/query/headers
- response diff: detect changes
פרמטרים נפוצים שנבדקים:
admin, isAdmin, is_admin, role, type, level, verified,
is_verified, email_verified, premium, group, permissions,
access_level, account_type, tier, balance, credit, discount,
debug, test, internal, hidden, secret
ניתוח תגובות API¶
GET /api/users/me HTTP/1.1
HTTP/1.1 200 OK
{
"id": 42,
"username": "wiener",
"email": "wiener@example.com",
"role": "user",
"isVerified": true,
"accountTier": "free",
"apiQuota": 100
}
התגובה חושפת שדות כמו role, accountTier, apiQuota שאולי ניתנים לשינוי דרך השמה המונית.
דוגמאות תקיפה מלאות¶
הסלמת הרשאות דרך הרשמה¶
POST /api/register HTTP/1.1
Content-Type: application/json
{
"username": "hacker",
"email": "hacker@evil.com",
"password": "password123",
"role": "admin"
}
הסלמת הרשאות דרך עדכון פרופיל¶
PUT /api/profile HTTP/1.1
Content-Type: application/json
Cookie: session=abc123
{
"username": "hacker",
"isAdmin": true
}
שינוי מחיר דרך עדכון מוצר¶
PUT /api/products/42 HTTP/1.1
Content-Type: application/json
{
"name": "Laptop",
"description": "Updated description",
"price": 0.01
}
שינוי בעלות על משאב¶
PUT /api/documents/100 HTTP/1.1
Content-Type: application/json
{
"title": "Updated Title",
"ownerId": 1
}
דוגמה מקיפה - אפליקציה פגיעה¶
const express = require('express');
const mongoose = require('mongoose');
// מודל משתמש עם שדות רגישים
const UserSchema = new mongoose.Schema({
username: { type: String, required: true },
email: { type: String, required: true },
password: { type: String, required: true },
role: { type: String, default: 'user', enum: ['user', 'editor', 'admin'] },
isVerified: { type: Boolean, default: false },
balance: { type: Number, default: 0 },
apiKey: { type: String, default: () => generateApiKey() },
permissions: { type: [String], default: ['read'] }
});
const User = mongoose.model('User', UserSchema);
const app = express();
app.use(express.json());
// פגיע - הרשמה
app.post('/api/register', async (req, res) => {
const user = new User(req.body); // כל השדות!
await user.save();
res.json(user);
});
// פגיע - עדכון פרופיל
app.put('/api/profile', async (req, res) => {
await User.findByIdAndUpdate(req.userId, req.body); // כל השדות!
res.json({ success: true });
});
// פגיע - עדכון חלקי
app.patch('/api/profile', async (req, res) => {
const user = await User.findById(req.userId);
Object.assign(user, req.body); // כל השדות!
await user.save();
res.json(user);
});
הגנות¶
רשימה לבנה - Whitelist (הדרך המומלצת)¶
// הגדרת שדות מותרים לכל פעולה
const ALLOWED_FIELDS = {
register: ['username', 'email', 'password'],
updateProfile: ['username', 'email', 'avatar', 'bio'],
updateSettings: ['theme', 'language', 'notifications']
};
function filterFields(body, allowedFields) {
const filtered = {};
for (const field of allowedFields) {
if (body[field] !== undefined) {
filtered[field] = body[field];
}
}
return filtered;
}
app.post('/api/register', async (req, res) => {
const data = filterFields(req.body, ALLOWED_FIELDS.register);
const user = new User(data);
await user.save();
res.json({ success: true });
});
שימוש ב-DTO - Data Transfer Objects¶
class RegisterDTO {
constructor(body) {
this.username = body.username;
this.email = body.email;
this.password = body.password;
// שדות נוספים לא מועתקים
}
validate() {
if (!this.username || !this.email || !this.password) {
throw new Error('Missing required fields');
}
// ולידציות נוספות
}
}
app.post('/api/register', async (req, res) => {
const dto = new RegisterDTO(req.body);
dto.validate();
const user = new User(dto);
await user.save();
});
הגדרת immutable fields ברמת המודל¶
const UserSchema = new mongoose.Schema({
username: String,
email: String,
password: String,
role: {
type: String,
default: 'user',
immutable: true // לא ניתן לשינוי אחרי יצירה
},
createdAt: {
type: Date,
default: Date.now,
immutable: true
}
});
סיכום¶
השמה המונית היא חולשה נפוצה בכל מסגרות העבודה. הנקודות המרכזיות:
- לעולם אל תעבירו את כל הפרמטרים ישירות למודל
- השתמשו ברשימה לבנה (whitelist) של שדות מותרים - לא ברשימה שחורה
- גלו שדות נסתרים דרך קוד JS, תיעוד API, הודעות שגיאה, ותגובות API
- השתמשו בכלי Param Miner לגילוי אוטומטי
- הגנה: Strong Parameters ב-Rails, ModelForm ב-Django, סינון מפורש ב-Express