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.
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/refundsbecause 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.
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.