جواب کوتاه: هیچوقت ورودی کاربر را مستقیم داخل رشتهی 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 همیشه با encodingـی که دیتابیس واقعاً استفاده میکند هماهنگ نیستند، و هر جایی که یک راه فراموش شود، حمله دوباره باز میشود. 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) سعی میکند دو مقدار را به یک ? بدهد و خطا میدهد. راه درست، ساختن همان تعداد ? که آرایه عضو دارد:
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 همینجور اشتباههای امنیتی رایج را قبل از پروداکشن نشانتان میدهد.





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