guest@websec: ~/websec/03-unsafe-query-generation/th — zsh — 100×40 เขียนโดย SaKKo ENTH โค้ด:

guest@websec ~ % man 03-unsafe-query-generation

03 · การสร้าง query ที่ไม่ปลอดภัย

query ของคุณ parameterize แล้ว แต่ยังได้แถวผิด ทำไม? เพราะตัว parse request แปลง "สตริง" เป็น null, array หรือ object

ปัญหา: ตัว parse request สร้างชนิดข้อมูลขึ้นมา

HTTP ส่งข้อความ แต่เฟรมเวิร์กช่วยแปลงเป็นชนิดอื่นให้:

requestparse ได้เป็นที่ไหน
?token=abc"abc"ทุกที่
?token[]=[""] หรือ [nil]Rails, PHP, Express (qs)
?token[]=a&token[]=b["a","b"]Rails, PHP, Express
?token[$ne]=x{ "$ne": "x" }Express (qs), PHP
{"token": null} (JSON body)nullJSON API ทุกตัว
{"token": {"$gt": ""}}objectJSON API ทุกตัว

แล้ว query builder ก็ทำหน้าที่ของมัน: null กลายเป็น IS NULL, array กลายเป็น IN (…) และใน MongoDB {"$ne": null} ตรงกับทุกคน

ทำไมจึงสำคัญ

การรีเซ็ตรหัสค้นผู้ใช้ด้วย token ผู้ใช้ส่วนใหญ่ไม่มี token (NULL) ส่ง null ไปก็ได้ผู้ใช้สุ่มสักคน แล้วรีเซ็ตรหัสเขา บั๊กนี้เคยเกิดกับ Rails มาแล้วหลายครั้ง

sim: การค้นหาเพื่อรีเซ็ตรหัสผ่านแก้ request แล้วดู query ที่ถูกสร้าง
try:
database:

ตาราง users

คำตอบของ Rails: deep_munge

deep_munge ของ Rails จัดการรูปแบบอย่าง [nil] และ params.permit(:token) ทิ้ง array กับ hash แต่ params[:token] ที่ใช้ตรงๆ ยังเป็นอะไรก็ได้ และ find_by(token: nil) ก็ยังหมายถึง IS NULL

ทางแก้ทั่วไป: ตรวจชนิดข้อมูลที่ขอบเขตระบบ

กำหนดรูปแบบของแต่ละพารามิเตอร์แล้วเช็กก่อน token ต้องเป็นสตริงที่ไม่ว่าง ถ้าไม่ใช่: 400

  • ใช้ schema validator (zod, pydantic หรือ strong params พร้อมเช็กชนิดใน Rails)
  • MongoDB: ตัดคีย์ที่ขึ้นต้นด้วย $ ออก
  • อย่าค้นหาด้วยค่าที่อาจว่าง
class PasswordsController < ApplicationController
  def update
    token = params[:token]
    return head :bad_request unless token.is_a?(String) && token.present?
    user = User.find_by_password_reset_token!(token)   # Rails 8: signed + expiring
    user.update!(params.permit(:password, :password_confirmation))
    redirect_to new_session_path, notice: "Password updated."
  rescue ActiveSupport::MessageVerifier::InvalidSignature
    redirect_to new_password_path, alert: "Link invalid or expired."
  end
end
# params.permit(:token) keeps only scalar values; arrays/hashes are dropped
import { z } from "zod";
const ResetBody = z.object({
  token: z.string().regex(/^[a-f0-9]{32}$/),     // exact shape, no null/array/object
  password: z.string().min(12).max(200),
});

app.post("/reset", async (req, res) => {
  const parsed = ResetBody.safeParse(req.body);
  if (!parsed.success) return res.status(400).json({ error: "Invalid request" });
  const { token, password } = parsed.data;
  const user = await User.findOne({ where: { resetToken: hash(token) } });
  if (!user) return res.status(400).json({ error: "Link invalid or expired" });
  …
});
from pydantic import BaseModel, Field, ValidationError

class ResetBody(BaseModel):
    token: str = Field(pattern=r"^[a-f0-9]{32}$")   # rejects None, lists, dicts
    password: str = Field(min_length=12, max_length=200)

@app.post("/reset")
def reset():
    try:
        body = ResetBody.model_validate(request.get_json())
    except ValidationError:
        return {"error": "Invalid request"}, 400
    user = User.query.filter_by(reset_token=hash_token(body.token)).first()
    if user is None:
        return {"error": "Link invalid or expired"}, 400
    …