לדלג לתוכן

השמה המונית - 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

GET /api/docs HTTP/1.1

תגובה: מסמך OpenAPI/Swagger שמפרט את כל השדות כולל שדות פנימיים

הודעות שגיאה מפורטות

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