آموزش

جلوگیری از SQL Injection در PHP با Prepared Statement

اگر کوئری‌تان را با چسباندن رشته می‌سازید، فرقی ندارد چقدر فرم زیبا طراحی کرده‌اید — یک آپاستروف در ورودی کافی است.

تحریریه‌ی دینا کد
نویسنده
۳ دقیقه مطالعه۰ بازدید
اشتراک گذاری۰

جواب کوتاه: هیچ‌وقت ورودی کاربر را مستقیم داخل رشته‌ی 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 همین‌جور اشتباه‌های امنیتی رایج را قبل از پروداکشن نشانتان می‌دهد.

تحریریه‌ی دینا کد
نویسنده

نظرها

اولین نفری باشید که نظر می‌دهد
برای ثبت نظر وارد شوید

سؤالتان را بپرسید یا تجربه‌تان را بنویسید — نویسنده‌ی مطلب پاسخ می‌دهد.

مطالب مرتبط

از همین دسته
آموزش

چرا Rust به‌جای segfault، خطای کامپایل می‌دهد

در C یا ++C یک اشاره‌گر نامعتبر معمولاً ماه‌ها بعد، در پروداکشن، خودش را نشان می‌دهد. در Rust همان اشتباه، همان لحظه‌ی کامپایل متوقفت می‌کند.

تحریریه‌ی دینا کد۵ دقیقه