فهرست مطالب
هیچوقت ورودی کاربر را مستقیم داخل رشتهی SQL نچسبان. همیشه PDO::prepare با پارامتر جایگذاریشده (? یا :name) بنویس؛ نه بهخاطر استایل، بلکه چون نسخهی رشتهای واقعاً هک میشود.
حملهای که واقعاً کار میکند
یک جدول کاربر با دو ردیف داریم. این کوئری، رمز را برای لاگین چک نمیکند، فقط برای نشاندادن مشکل ساده شده:
php$username = "' OR '1'='1";
$sql = "SELECT * FROM users WHERE username = '$username'";
$rows = $pdo->query($sql)->fetchAll();
کوئری نهایی که اجرا میشود:
sqlSELECT * FROM users WHERE username = '' OR '1'='1'
rows returned: 2
- admin
- sara
ورودی حتی رمز عبور هم نداشت؛ فقط یک آپاستروف و یک شرطِ همیشهدرست کافی بود تا کل جدول کاربران، از جمله admin، برگردد.
همان ورودی، از یک prepared statement رد میشود
php$stmt = $pdo->prepare('SELECT * FROM users WHERE username = ?');
$stmt->execute([$username]);
$rows = $stmt->fetchAll();
rows returned: 0
با prepared statement، مقدار $username هیچوقت بهعنوان بخشی از متن SQL دیده نمیشود؛ جدا از متن کوئری به دیتابیس فرستاده میشود، پس آپاستروف داخلش فقط یک کاراکتر عادی است و syntax را نمیشکند.
چرا escapeکردن دستی کافی نیست
توابعی مثل addslashes همیشه با انکودینگ واقعی دیتابیس هماهنگ نیستند؛ هر جایی که یک راه فراموش شود، حمله دوباره باز میشود. Prepared statement این مشکل را از ریشه حذف میکند؛ چیزی برای فراموشکردن باقی نمیماند، چون داده هیچوقت وارد متن کوئری نمیشود.
قانون عملی برای کل پروژه
- هر جا کوئری با متغیر کاربر ساخته میشود، یا
prepareاست یا باگ. - اسم جدول و ستون را نمیشود parameterize کرد؛ آنها را از یک allowlist ثابت در کد بردار، نه از ورودی کاربر.
- ORMها (مثل Eloquent یا Doctrine) این کار را زیر کاپوت انجام میدهند؛ خطر واقعی جایی است که کسی برای «سرعت» یک کوئری خام با رشته مینویسد.
حتی با prepare درست، یک آرایه را نمیشود به IN(...) بایند کرد
php$ids = [1, 2];
$stmt = $pdo->prepare('SELECT * FROM users WHERE id IN (?)');
$stmt->execute($ids);
// PDOException: SQLSTATE[HY000]: General error: 25 column index out of range
یک placeholder فقط یک مقدار میگیرد، نه یک آرایه؛ execute($ids) سعی میکند دو مقدار را به یک ? بدهد و خطا میدهد. پیام خطا به درایور بستگی دارد؛ خروجی بالا را PDO با SQLite میدهد و روی MySQL همان جایگذاری اشتباه با کد HY093 و پیام Invalid parameter number برگشت میخورد. راه درست، ساختن همان تعداد ? که آرایه عضو دارد:
php$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);
// rows: 2
اسم ستون و تعداد ? از کد میآید، نه از ورودی کاربر؛ فقط خودِ مقادیر آرایه پارامتر میشوند. قاعدهی همیشگی هم همین است: داده جای داده، ساختار کوئری جای کد.
اگر PHP را از پایه و با پروژهی واقعی یاد میگیری، دورهی PHP همینجور اشتباههای امنیتی رایج را قبل از پروداکشن نشانت میدهد.




نظرها
اولین نفری باشید که نظر میدهدسؤالتان را بپرسید یا تجربهتان را بنویسید تا نویسندهی مطلب پاسخ بدهد.