SQL Injection trong Rails: parameter binding, query API và những lối tắt nguy hiểm

SQL injection xảy ra khi dữ liệu không đáng tin được ghép vào cấu trúc SQL, khiến input của người dùng biến thành một phần câu lệnh.

1
2
# Nguy hiểm
User.where("email = '#{params[:email]}'")

Kẻ tấn công không cần đội hoodie trong phòng tối. Một dấu nháy đúng chỗ thường đã đủ làm ngày của bạn dài hơn.

Dùng parameter binding

1
2
User.where("email = ?", params[:email])
User.where(email: params[:email])

ActiveRecord tách cấu trúc query khỏi value và bind parameter phù hợp.

Những API vẫn cần cảnh giác

Không phải method ActiveRecord nào cũng tự sanitize mọi string:

1
2
3
User.order(params[:sort])
User.select(params[:columns])
User.from(params[:table])

Tên cột, hướng sort và SQL fragment là identifier/cấu trúc, không phải value bind thông thường. Hãy dùng allowlist:

1
2
3
4
5
6
7
SORT_COLUMNS = {
"name" => :name,
"created_at" => :created_at
}.freeze

column = SORT_COLUMNS.fetch(params[:sort], :created_at)
User.order(column => :desc)

Raw SQL

Nếu thật sự cần SQL thủ công, vẫn bind parameter:

1
2
3
4
5
6
7
sql = User.sanitize_sql_array([
"email = ? AND active = ?",
params[:email],
true
])

User.where(sql)

Tốt hơn nữa, dùng adapter/query API hỗ trợ bind thay vì tự escape. Không tự viết hàm thay ' thành ''; bảo mật không nên phụ thuộc vào một bài replace rất tự tin.

Defense in depth

  • Database user chỉ có quyền cần thiết.
  • Không trả database error chi tiết ra client.
  • Log và monitor query bất thường nhưng không log sensitive input.
  • Validate input để bảo vệ domain; parameterize để bảo vệ SQL. Hai việc khác nhau.
  • Test các query dynamic, đặc biệt sort/filter/reporting.

Prepared statement không sửa được logic authorization. Query an toàn vẫn có thể trả dữ liệu của người khác nếu policy sai.

References