تزریق SQL چیست و چرا باید جدی گرفته شود؟
تزریق SQL (SQL Injection) یکی از قدیمیترین و در عین حال خطرناکترین حملات به وبسایتهاست. در این حمله، مهاجم با ارسال ورودیهای خاص به فرمها، پارامترهای URL یا حتی هدرهای HTTP، کدهای SQL دلخواه خود را به کوئریهای پایگاهداده تزریق میکند. اگر برنامه شما ورودی کاربر را بدون پاکسازی یا اعتبارسنجی مستقیم در کوئری قرار دهد، مهاجم میتواند دادهها را بخواند، تغییر دهد، حذف کند یا حتی کل پایگاهداده را در اختیار بگیرد.
طبق آمار OWASP، تزریق SQL سالهاست که در فهرست ده خطر برتر امنیت وب قرار دارد. یک اشتباه کوچک در نوشتن کوئری میتواند منجر به نشت اطلاعات حساس کاربران، سرقت رمزهای عبور، یا تخریب کامل دادهها شود. در این مقاله به زبان ساده و با مثالهای عملی نشان میدهیم که چرا کوئری پارامتری (Parameterized Query) تنها راهحل واقعی و قطعی برای مقابله با این حمله است.
مکانیزم حمله تزریق SQL را بشناسید
برای درک اهمیت کوئری پارامتری، ابتدا باید ببینیم حمله چگونه کار میکند. فرض کنید یک فرم ورود ساده دارید که کاربر نام کاربری و رمز عبور را وارد میکند. کد PHP شما ممکن است به این شکل باشد:
<?php
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);
?>
حال اگر مهاجم در فیلد نام کاربری مقدار زیر را وارد کند:
' OR '1'='1' --
کوئری نهایی به این شکل در میآید:
SELECT * FROM users WHERE username = '' OR '1'='1' -- ' AND password = ''
بخش -- در SQL یعنی «تا آخر خط کامنت است»، بنابراین شرط رمز عبور کاملاً نادیده گرفته میشود. عبارت '1'='1' همیشه درست است، پس کوئری تمام رکوردهای جدول users را برمیگرداند و مهاجم بدون دانستن رمز عبور وارد حساب کاربری میشود. این سادهترین نوع حمله است؛ حملات پیشرفتهتر میتوانند دادهها را استخراج کنند، جداول را حذف کنند یا حتی از طریق UNION SELECT اطلاعات سایر جداول را بخوانند.
انواع رایج تزریق SQL
- In-band SQLi: مهاجم نتیجه را مستقیماً در پاسخ HTTP میبیند. شامل دو نوع Error-based و Union-based است.
- Blind SQLi: نتیجه به صورت مستقیم نمایش داده نمیشود، اما مهاجم با مشاهده تفاوت در رفتار برنامه (مثلاً تأخیر زمانی یا پیام خطا) اطلاعات را حدس میزند.
- Out-of-band SQLi: دادهها از طریق کانال دیگری مثل DNS یا HTTP request به سرور مهاجم ارسال میشوند.
همه این انواع یک نقطه اشتراک دارند: برنامه، ورودی کاربر را به عنوان بخشی از دستور SQL تفسیر میکند. راهحل اساسی این است که بین «دستور» و «داده» تفکیک قائل شویم.
چرا روشهای سنتی دفاع کافی نیستند؟
بسیاری از توسعهدهندگان برای جلوگیری از تزریق SQL از روشهایی مثل فرار دادن کاراکترها (Escaping) یا فیلتر کردن ورودی استفاده میکنند. اما این روشها ذاتاً ناقص هستند و بارها شکست خوردهاند.
مشکل با mysqli_real_escape_string
تابع mysqli_real_escape_string کاراکترهای خاص مثل '، " و \ را با بکاسلش فرار میدهد. اما اگر کاراکتر ست (Character Set) پایگاهداده به درستی تنظیم نشده باشد، مهاجم میتواند از ترفندهایی مثل GBK یا UTF-8 برای دور زدن این فرار استفاده کند. مثال معروف حمله با %bf%27 در MySQL با کاراکتر ست GBK نشان میدهد که این روش به تنهایی قابل اعتماد نیست.
مشکل با فیلتر کردن ورودی
برخی توسعهدهندگان سعی میکنند کلمات کلیدی مثل SELECT، UNION یا DROP را از ورودی حذف کنند. این روش هم شکستپذیر است؛ مهاجم میتواند از تکنیکهای Encoding، فاصلههای تکراری، کامنتها یا ترکیب حروف بزرگ و کوچک برای دور زدن فیلترها استفاده کند. به عنوان مثال، SeLeCt یا SELECT/**/FROM به راحتی از فیلترهای ساده عبور میکنند.
نکته مهم این است که امنیت نباید بر پایه «لیست سیاه» (Blacklist) باشد، بلکه باید بر پایه «لیست سفید» (Whitelist) و تفکیک دستور از داده بنا شود. کوئری پارامتری دقیقاً همین کار را انجام میدهد.
کوئری پارامتری چیست و چگونه کار میکند؟
کوئری پارامتری (Parameterized Query) یا Prepared Statement روشی است که در آن ساختار کوئری SQL ابتدا تعریف میشود و سپس مقادیر به صورت جداگانه و با استفاده از placeholder به آن ارسال میشوند. پایگاهداده این مقادیر را به عنوان «داده» تفسیر میکند، نه به عنوان بخشی از «دستور». این تفکیک ذاتی باعث میشود که تزریق SQL از نظر فنی غیرممکن شود.
مثال عملی با PHP و MySQLi
بیایید همان مثال فرم ورود را با کوئری پارامتری بازنویسی کنیم:
<?php
$username = $_POST['username'];
$password = $_POST['password'];
// آمادهسازی کوئری با placeholder
$stmt = $conn->prepare("SELECT * FROM users WHERE username = ? AND password = ?");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
$result = $stmt->get_result();
?>
در این کد، علامت ? به عنوان placeholder عمل میکند. تابع bind_param نوع داده (در اینجا s برای string) و مقدار را مشخص میکند. پایگاهداده MySQL این مقادیر را به عنوان داده خام دریافت میکند و هرگز آنها را به عنوان دستور SQL اجرا نمیکند. حتی اگر مهاجم مقدار ' OR '1'='1' -- را وارد کند، این رشته به عنوان یک مقدار عادی برای فیلد username در نظر گرفته میشود و هیچ تأثیری روی ساختار کوئری ندارد.
مثال با PDO (PHP Data Objects)
PDO روش مدرنتری برای کار با پایگاهداده است و از کوئری پارامتری به صورت استاندارد پشتیبانی میکند:
<?php
$pdo = new PDO("mysql:host=localhost;dbname=mydb;charset=utf8mb4", $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute(['username' => $_POST['username'], 'password' => $_POST['password']]);
$user = $stmt->fetch();
?>
در اینجا از placeholderهای نامدار (:username و :password) استفاده شده که خوانایی کد را افزایش میدهد. تابع execute مقادیر را به صورت آرایه دریافت میکند و PDO به طور خودکار آنها را به درستی به پایگاهداده ارسال میکند.
کوئری پارامتری در سایر زبانها و فریمورکها
این الگو فقط محدود به PHP نیست. در تمام زبانها و فریمورکهای مدرن، کوئری پارامتری به عنوان استاندارد طلایی امنیت شناخته میشود.
- Python (با psycopg2 یا sqlite3): از
%sیا?به عنوان placeholder استفاده کنید. - Node.js (با mysql2 یا pg): از
?یا$1استفاده کنید. - Java (با JDBC): از
PreparedStatementاستفاده کنید. - فریمورکهایی مثل Laravel، Django، Rails: به صورت پیشفرض از Query Builder یا ORM استفاده میکنند که کوئری پارامتری را به طور خودکار اعمال میکند.
اگر از فریمورک استفاده میکنید، تقریباً همیشه متدهای آمادهای برای این کار وجود دارد. به عنوان مثال در Laravel، DB::select('SELECT * FROM users WHERE id = ?', [$id]) به صورت خودکار از کوئری پارامتری استفاده میکند.
اشتباهات رایج و نکات عیبیابی
حتی وقتی از کوئری پارامتری استفاده میکنید، ممکن است اشتباهاتی رخ دهد که امنیت را به خطر بیندازد. در اینجا چند مورد رایج را بررسی میکنیم.
اشتباه: استفاده از کوئری پارامتری فقط برای بخشی از کوئری
برخی توسعهدهندگان فقط مقادیر WHERE را پارامتری میکنند اما نام جدول یا ستون را به صورت مستقیم از ورودی کاربر میسازند. به این مثال توجه کنید:
<?php
$table = $_GET['table']; // ورودی کاربر
$stmt = $pdo->prepare("SELECT * FROM $table WHERE id = ?");
$stmt->execute([$id]);
?>
در اینجا مهاجم میتواند مقدار table را به users; DROP TABLE users; -- تغییر دهد. کوئری پارامتری فقط برای مقادیر کار میکند، نه برای شناسهگرها (Identifiers). برای نام جدول و ستون باید از لیست سفید استفاده کنید:
<?php
$allowedTables = ['users', 'products', 'orders'];
if (!in_array($table, $allowedTables)) {
die("Invalid table name");
}
?>
اشتباه: فراموش کردن تنظیم کاراکتر ست
اگر کاراکتر ست اتصال پایگاهداده را به درستی تنظیم نکنید، حتی کوئری پارامتری هم ممکن است در برخی موارد آسیبپذیر باشد. همیشه از utf8mb4 استفاده کنید و در PDO آن را در DSN تنظیم کنید:
$pdo = new PDO("mysql:host=localhost;dbname=mydb;charset=utf8mb4", $user, $pass);
اشتباه: اعتماد به ORM بدون درک عملکرد آن
بسیاری از ORMها به شما اجازه میدهند کوئری خام بنویسید. اگر از این امکان استفاده میکنید، باید همان قوانین کوئری پارامتری را رعایت کنید. به عنوان مثال در Laravel، متد whereRaw به شما اجازه میدهد کوئری خام بنویسید؛ اگر ورودی کاربر را مستقیم در آن قرار دهید، همان خطر تزریق SQL وجود دارد.
لایههای دفاعی مکمل
کوئری پارامتری خط مقدم دفاع است، اما امنیت عمیق (Defense in Depth) ایجاب میکند که لایههای دیگری هم داشته باشید.
- اعتبارسنجی ورودی: حتی با کوئری پارامتری، ورودیها را از نظر نوع، طول و قالب بررسی کنید. برای مثال، فیلد ایمیل باید با regex بررسی شود.
- حداقل دسترسی پایگاهداده: کاربر پایگاهداده برنامه نباید دسترسی DROP یا ALTER داشته باشد. فقط دسترسیهای لازم (SELECT، INSERT، UPDATE، DELETE) را بدهید.
- رمزنگاری دادههای حساس: رمزهای عبور را با
password_hashهش کنید و هرگز به صورت متن ساده ذخیره نکنید. - لاگگیری و مانیتورینگ: تلاشهای مشکوک برای تزریق SQL را در لاگها ثبت کنید و هشدار دریافت کنید.
جمعبندی: کوئری پارامتری را به عادت تبدیل کنید
تزریق SQL یک تهدید جدی است که سالانه میلیونها وبسایت را تحت تأثیر قرار میدهد. روشهای سنتی مثل فرار دادن کاراکترها و فیلتر کردن ورودی، راهحلهای موقتی و ناقص هستند. تنها راهحل واقعی و قطعی، استفاده از کوئری پارامتری (Prepared Statement) است که تفکیک ذاتی بین دستور و داده را فراهم میکند.
این الگو را در تمام لایههای برنامه خود اعمال کنید: فرمهای ورود، جستجو، فیلترها، مرتبسازی و هر جایی که با پایگاهداده تعامل دارید. اگر از فریمورک استفاده میکنید، از امکانات داخلی آن بهره ببرید و اگر کد خام مینویسید، همیشه از Prepared Statement استفاده کنید. با این کار، یکی از مهمترین گامها را برای ایمنسازی وبسایت خود برداشتهاید.
اگر به دنبال زیرساخت امن برای میزبانی وبسایت خود هستید، سرورنت (ServerNet) خدمات میزبانی وب و سرور ابری را با تمرکز بر امنیت ارائه میدهد که میتواند به شما در اجرای برنامههای امن کمک کند.
دیدگاهها ۰
هنوز دیدگاهی ثبت نشده؛ اولین نفر باشید!