guest@websec: ~/websec/09-mass-assignment-and-authorization — zsh — 100×40 by SaKKo ENTH code:

guest@websec ~ % man 09-mass-assignment-and-authorization

09 · Mass Assignment and Authorization

Broken access control is the #1 web risk. Two questions on every request: which fields may this user set, and what may they do to which records?

9.1 Mass assignment

It's tempting to copy the whole form onto the model: User.update(params[:user]). But the attacker writes that form. They can just add role: "admin".

This happened to GitHub in 2012, and it's why Rails has strong parameters.

sim: profile update requestedit the JSON the browser sends
controller uses:

user record after save

Strong params are on by default: raw params raise an error, so you list allowed keys with permit or params.expect. The hole is permit! (allows everything), and permit lists that include :role. Brakeman flags both.
# Rails 8: expect = require + permit, and rejects wrongly shaped params with 400
def profile_params
  params.expect(user: [:name, :email])
end

# admin-only fields get their own permit list in the admin controller
def admin_user_params
  params.expect(user: [:name, :email, :role])
end

params.require(:user).permit!            # permits everything: never on user input
// JS stacks have the same bug: Object.assign(user, req.body) or ORM.update(req.body)
const { name, email } = req.body;              // pick fields explicitly
await User.update({ name, email }, { where: { id: req.user.id } });
await User.update(req.body, { where: { id: req.user.id } });   // mass assignment

9.2 Authorization: objects and functions

Authentication = who you are. Authorization = what you may do. Two ways it breaks:

  • Wrong record (BOLA/IDOR): allowed to view invoices, just not that one.
  • Wrong action (BFLA): a normal user calls POST /admin/refunds because we only hid the button.

Also watch for: changing account_id in a request, skipping steps in a flow, and API endpoints that missed the checks the HTML page has.

sim: who can do what?pick a role and an action
Nothing built in. Scope through Current.user and use Pundit or Action Policy with verify_authorized, so a forgotten check fails loudly.
class ApplicationController < ActionController::Base
  include Pundit::Authorization
  after_action :verify_authorized, except: :index     # forgot authorize? → error
  after_action :verify_policy_scoped, only: :index
end

class RefundsController < ApplicationController
  def create
    order = policy_scope(Order).find(params[:order_id])  # object level (BOLA)
    authorize order, :refund?                            # function level (BFLA)
    order.refund!(amount: params.expect(:amount))
  end
end

class OrderPolicy < ApplicationPolicy
  def refund? = user.manager? && record.account_id == user.account_id
  class Scope < ApplicationPolicy::Scope
    def resolve = scope.where(account_id: user.account_id)   # tenant isolation
  end
end

9.3 Designing authorization that holds

  • Deny by default. No rule, no access.
  • Check on the server, every time. Hidden buttons aren't security.
  • One place for rules (policy objects), not if admin? everywhere.
  • Take the account from the session, never from params.
  • Test it: sign in as each role and check you get 403/404.
  • Prefer 404 over 403 so IDs can't be probed.