guest@websec ~ % man 09-mass-assignment-and-authorization
09 · Mass Assignment และ Authorization
Broken access control คือความเสี่ยงอันดับ 1 ของเว็บ ทุก request ต้องตอบสองคำถาม: ผู้ใช้นี้ตั้งค่าฟิลด์ไหนได้ และทำอะไรกับเรคคอร์ดไหนได้?
9.1 Mass assignment
เขียน User.update(params[:user]) แล้วคัดลอกทั้งฟอร์มลงโมเดลมันง่าย แต่ผู้โจมตีเป็นคนเขียนฟอร์มนั้น เขาแค่เพิ่ม role: "admin" เข้าไป
เรื่องนี้เกิดกับ GitHub ในปี 2012 และเป็นเหตุผลที่ Rails มี strong parameters
controller uses:
เรคคอร์ดผู้ใช้หลังบันทึก
Strong params เปิดไว้แล้ว: ส่ง params ดิบจะ error คุณต้องระบุคีย์ที่อนุญาตด้วย
permit หรือ params.expect ช่องโหว่คือ permit! (อนุญาตทุกอย่าง) และรายการ permit ที่มี :role Brakeman จับได้ทั้งสองแบบ# 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: ระดับ object และระดับฟังก์ชัน
Authentication = คุณคือใคร Authorization = คุณทำอะไรได้ มีสองแบบที่พัง:
- ผิดเรคคอร์ด (BOLA/IDOR): ดูใบแจ้งหนี้ได้ แต่ไม่ใช่ ใบนั้น
- ผิดการกระทำ (BFLA): ผู้ใช้ทั่วไปเรียก
POST /admin/refundsได้ เพราะเราแค่ซ่อนปุ่ม
ระวังด้วย: การแก้ account_id ใน request การข้ามขั้นตอน และ API endpoint ที่ไม่ได้ตรวจเหมือนหน้า HTML
ไม่มีในตัว ให้ค้นผ่าน
Current.user และใช้ Pundit หรือ Action Policy พร้อม verify_authorized เพื่อให้การลืมตรวจล้มเหลวแบบเห็นชัด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 ออกแบบ authorization ให้แข็งแรง
- ปฏิเสธเป็นค่าเริ่มต้น ไม่มีกฎ ก็ไม่ได้สิทธิ์
- ตรวจที่เซิร์ฟเวอร์ทุกครั้ง การซ่อนปุ่มไม่ใช่ความปลอดภัย
- รวมกฎไว้ที่เดียว (policy object) ไม่ใช่
if admin?กระจายทั่ว - เอาบัญชีจาก session ห้ามเอาจาก params
- ทดสอบ: ล็อกอินเป็นแต่ละบทบาทแล้วเช็กว่าได้ 403/404
- ใช้ 404 แทน 403 เพื่อไม่ให้ไล่เดา ID ได้