לדלג לתוכן

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

הבעיה

# Vulnerable code - Rails < 4.0
class User < ActiveRecord::Base
  # All fields can be updated
end

class UsersController < ApplicationController
  def create
    # Vulnerable - all parameters are passed directly
    @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
    # Protected - only permitted parameters
    @user = User.new(user_params)
    @user.save
  end

  private

  def user_params
    params.require(:user).permit(:username, :email, :password)
    # role, is_admin, balance - not permitted!
  end
end

ההגנה הישנה - attr_accessible (Rails < 4.0)

class User < ActiveRecord::Base
  # Only these fields are allowed for mass assignment
  attr_accessible :username, :email, :password

  # Or conversely - block sensitive fields
  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 - vulnerable
def register(request):
    if request.method == 'POST':
        # Vulnerable - all fields are passed
        user = User(**request.POST.dict())
        user.save()
        return redirect('/dashboard')

ההגנה - ModelForm עם fields

# forms.py - protected
class UserRegistrationForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['username', 'email', 'password']
        # Only these fields are allowed

    # Don't use exclude - prefer fields!
    # exclude = ['role', 'is_staff']  # dangerous - new fields won't be blocked

# views.py - protected
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 - vulnerable
app.post('/register', async (req, res) => {
    // Vulnerable - all parameters are passed directly to the model
    const user = new User(req.body);
    await user.save();
    res.json({ success: true });
});

// Also vulnerable on update
app.put('/profile', async (req, res) => {
    // Vulnerable - updates every field that was sent
    await User.findByIdAndUpdate(req.session.userId, req.body);
    res.json({ success: true });
});

ההגנה - סינון שדות מפורש

// routes/users.js - protected
app.post('/register', async (req, res) => {
    // Protected - only allowed fields
    const { username, email, password } = req.body;
    const user = new User({ username, email, password });
    await user.save();
    res.json({ success: true });
});

// Protected profile update
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 {
    // Vulnerable - no restriction on fields
    protected $guarded = [];

    // or:
    // without defining $fillable at all
}

// app/Http/Controllers/UserController.php - vulnerable
public function store(Request $request) {
    // Vulnerable - all parameters are passed
    $user = User::create($request->all());
    return response()->json($user);
}

ההגנה - fillable ו-guarded

// app/Models/User.php - protected
class User extends Model {
    // Approach 1: whitelist - only these fields are allowed for assignment
    protected $fillable = ['username', 'email', 'password'];

    // Approach 2: blacklist - these fields are blocked
    // protected $guarded = ['role', 'is_admin', 'balance'];
    // Less recommended - new fields won't be blocked automatically

    // Hidden fields (not returned in JSON)
    protected $hidden = ['password', 'api_key', 'reset_token'];
}

גילוי פרמטרים נסתרים

קריאת קוד JavaScript בצד הלקוח

// Sometimes client-side code exposes the model structure
// Look in JS files:

// Example 1: a user object in the code
const user = {
    username: "",
    email: "",
    password: "",
    role: "user",       // internal field!
    isAdmin: false,     // internal field!
    accountType: "free" // internal field!
};

// Example 2: client-side validation that exposes fields
function validateForm(data) {
    if (data.role && !['user', 'admin'].includes(data.role)) {
        // exposes the allowed values of role
    }
}

קריאת תיעוד API

GET /api/docs HTTP/1.1

Response: an OpenAPI/Swagger document detailing all fields, including internal ones

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

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

Commonly tested parameters:
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');

// User model with sensitive fields
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());

// Vulnerable - registration
app.post('/api/register', async (req, res) => {
    const user = new User(req.body);  // all fields!
    await user.save();
    res.json(user);
});

// Vulnerable - profile update
app.put('/api/profile', async (req, res) => {
    await User.findByIdAndUpdate(req.userId, req.body);  // all fields!
    res.json({ success: true });
});

// Vulnerable - partial update
app.patch('/api/profile', async (req, res) => {
    const user = await User.findById(req.userId);
    Object.assign(user, req.body);  // all fields!
    await user.save();
    res.json(user);
});

הגנות

רשימה לבנה - Whitelist (הדרך המומלצת)

// Define allowed fields for each action
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;
        // Additional fields are not copied
    }

    validate() {
        if (!this.username || !this.email || !this.password) {
            throw new Error('Missing required fields');
        }
        // Additional validations
    }
}

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  // cannot be changed after creation
    },
    createdAt: {
        type: Date,
        default: Date.now,
        immutable: true
    }
});

סיכום

השמה המונית היא חולשה נפוצה בכל מסגרות העבודה. הנקודות המרכזיות:

  • לעולם אל תעבירו את כל הפרמטרים ישירות למודל
  • השתמשו ברשימה לבנה (whitelist) של שדות מותרים - לא ברשימה שחורה
  • גלו שדות נסתרים דרך קוד JS, תיעוד API, הודעות שגיאה, ותגובות API
  • השתמשו בכלי Param Miner לגילוי אוטומטי
  • הגנה: Strong Parameters ב-Rails, ModelForm ב-Django, סינון מפורש ב-Express