SQL Injection چیست و چگونه از آن جلوگیری کنیم؟

یک ورودی ساده در سایت می‌تواند به نقطه شروع یک حمله جدی به پایگاه داده تبدیل شود. SQL Injection یکی از آسیب‌پذیری‌های شناخته‌شده وب است که با یک پیاده‌سازی نادرست می‌تواند اطلاعات حساس را در معرض خطر قرار دهد. اما این حمله چگونه شکل می‌گیرد و چطور می‌توان جلوی آن را گرفت؟

SQL Injection یکی از مهم‌ترین آسیب‌پذیری‌های امنیتی در برنامه‌های تحت وب است. این آسیب‌پذیری زمانی شکل می‌گیرد که داده‌ای که از کاربر، URL، فرم، API یا منبع خارجی دریافت شده، بدون جداسازی صحیح وارد دستور SQL شود.

مشکل اصلی زمانی ایجاد می‌شود که برنامه نتواند بین داده ورودی و بخش اجرایی Query مرز مشخصی ایجاد کند. در این وضعیت، ورودی کنترل‌نشده می‌تواند روی منطق Query اثر بگذارد.

OWASP برای مقابله با SQL Injection استفاده از Prepared Statements با Parameterized Queries را به عنوان راهکار اصلی پیشنهاد می‌کند. در این روش، ساختار SQL از مقادیر ورودی جدا می‌شود و داده کاربر به عنوان بخشی از دستور SQL تفسیر نمی‌شود.

SQL Injection چگونه اتفاق می‌افتد؟

فرض کنید برنامه‌ای برای دریافت اطلاعات یک کاربر، Query زیر را تولید می‌کند:

SELECT id, username
FROM users
WHERE username = 'USER_INPUT';

اگر برنامه مقدار USER_INPUT را با اتصال مستقیم رشته‌ها تولید کند، داده ورودی وارد ساختار SQL شده است.

یک پیاده‌سازی ناامن در PHP می‌تواند چنین شکلی داشته باشد:

$username = $_GET['username'];

$sql = "SELECT id, username
        FROM users
        WHERE username = '$username'";

$result = mysqli_query($connection, $sql);

اینجا مشکل اصلی این است که مقدار دریافت‌شده از کاربر مستقیماً داخل Query قرار گرفته است.

OWASP نیز نمونه مشابهی را به عنوان الگوی رایج SQL Injection معرفی می‌کند که در آن پارامتر کنترل‌نشده به Query متصل می‌شود.

چرا این روش خطرناک است؟

SQL یک زبان اجرایی است. اگر داده و کد بدون مرزبندی مناسب کنار یکدیگر قرار بگیرند، پایگاه داده ممکن است بخشی از ورودی را به عنوان syntax مربوط به SQL تفسیر کند.

در معماری امن، برنامه باید Query را از قبل مشخص کند و مقدار کاربر را به عنوان پارامتر به آن تحویل دهد.

Prepared Statement چیست؟

Prepared Statement روشی است که در آن ساختار SQL از مقادیری که قرار است در Query قرار بگیرند جدا می‌شود.

برای مثال در PDO:

$stmt = $pdo->prepare(
    'SELECT id, username
     FROM users
     WHERE username = :username'
);

$stmt->execute([
    'username' => $username
]);

در اینجا :username یک Parameter است و مقدار واقعی هنگام اجرای Statement ارسال می‌شود.

مستندات PHP نیز استفاده از پارامترها برای ورودی‌های کاربر را توصیه می‌کنند و تأکید دارند که داده کاربر نباید مستقیماً داخل Query قرار گیرد.

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

یکی از روش‌های استاندارد در PHP استفاده از PDO است.

نمونه کامل‌تر:

<?php

$pdo = new PDO(
    'mysql:host=localhost;dbname=app;charset=utf8mb4',
    'app_user',
    'password'
);

$pdo->setAttribute(
    PDO::ATTR_ERRMODE,
    PDO::ERRMODE_EXCEPTION
);

$username = $_POST['username'] ?? '';

$stmt = $pdo->prepare(
    'SELECT id, username
     FROM users
     WHERE username = :username'
);

$stmt->execute([
    'username' => $username
]);

$user = $stmt->fetch(PDO::FETCH_ASSOC);

در این ساختار، مقدار $username مستقیماً به Query متصل نشده است.

PDO::prepare() الگوی Query را دریافت می‌کند و execute() مقادیر واقعی را برای پارامترها ارسال می‌کند. مستندات PHP همچنین تأکید می‌کنند که Placeholderها برای مقادیر داده‌ای هستند و نمی‌توان از آن‌ها برای جایگزینی نام جدول، نام ستون یا بخش دلخواهی از ساختار SQL استفاده کرد.

یک نکته مهم درباره Placeholderهای PDO

این روش صحیح است:

$stmt = $pdo->prepare(
    'SELECT * FROM users WHERE id = :id'
);

$stmt->execute([
    'id' => $id
]);

اما نمی‌توان انتظار داشت چنین چیزی برای نام جدول کار کند:

$table = 'users';

$stmt = $pdo->prepare(
    'SELECT * FROM :table'
);

Parameter برای یک مقدار داده‌ای است، نه Identifier.

مستندات PDO نیز این محدودیت را صراحتاً مشخص کرده‌اند.

جلوگیری از SQL Injection با MySQLi

در PHP می‌توان از MySQLi نیز برای Prepared Statement استفاده کرد.

نمونه استاندارد:

<?php

$mysqli = new mysqli(
    'localhost',
    'app_user',
    'password',
    'app'
);

$mysqli->set_charset('utf8mb4');

$username = $_POST['username'] ?? '';

$stmt = $mysqli->prepare(
    'SELECT id, username
     FROM users
     WHERE username = ?'
);

$stmt->bind_param('s', $username);
$stmt->execute();

$result = $stmt->get_result();

$user = $result->fetch_assoc();

در MySQLi، علامت ? به عنوان Placeholder استفاده می‌شود. این پارامتر باید قبل از اجرای Query با bind_param() به یک متغیر متصل شود. مستندات رسمی PHP همین الگوی prepare، bind_param و execute را برای Prepared Statement تعریف می‌کنند.

پارامتر اول bind_param() نوع داده را مشخص می‌کند.

i = integer
d = double
s = string
b = binary

برای نمونه:

$stmt->bind_param('i', $user_id);

برای یک شناسه عددی استفاده می‌شود.

اعتبارسنجی ورودی چه نقشی دارد؟

Validation یکی دیگر از لایه‌های امنیتی مهم است، اما نباید آن را جایگزین Prepared Statement دانست.

فرض کنید پارامتر id باید حتماً یک عدد صحیح باشد:

$id = filter_input(
    INPUT_GET,
    'id',
    FILTER_VALIDATE_INT
);

if ($id === false || $id === null) {
    http_response_code(400);
    exit('Invalid ID');
}

حالا Query نیز باید به صورت پارامتری اجرا شود:

$stmt = $pdo->prepare(
    'SELECT id, name, price
     FROM products
     WHERE id = :id'
);

$stmt->execute([
    'id' => $id
]);

در اینجا دو کنترل داریم.

Validation بررسی می‌کند که ورودی از نظر نوع و قواعد مورد انتظار معتبر باشد.

Parameterized Query مانع آن می‌شود که مقدار ورودی به عنوان syntax مربوط به SQL تفسیر شود.

OWASP نیز تأکید می‌کند که Validation به تنهایی تضمین نمی‌کند داده برای قرار گرفتن در یک Query ساخته‌شده با String Concatenation امن باشد.

Allowlist چیست و چه زمانی استفاده می‌شود؟

همه بخش‌های SQL را نمی‌توان با Placeholder پارامتری کرد.

برای مثال تصور کنید کاربر باید یکی از گزینه‌های مرتب‌سازی را انتخاب کند:

name
price
date

نمی‌توان صرفاً انتظار داشت نام ستون را با یک Parameter معمولی جایگزین کنیم.

در این شرایط، Allowlist راهکار مناسبی است.

$allowedSorts = [
    'name'  => 'name',
    'price' => 'price',
    'date'  => 'created_at'
];

$sort = $_GET['sort'] ?? 'date';

$orderBy = $allowedSorts[$sort] ?? 'created_at';

$stmt = $pdo->prepare(
    "SELECT id, name, price
     FROM products
     ORDER BY $orderBy"
);

$stmt->execute();

کاربر فقط می‌تواند یکی از کلیدهای تعریف‌شده را انتخاب کند.

مقدار ورودی مستقیماً به SQL منتقل نمی‌شود.

OWASP استفاده از Allowlist را برای بخش‌هایی مانند نام جدول، نام ستون و ترتیب ASC یا DESC که معمولاً محل مناسبی برای Bind Variable نیستند، توصیه می‌کند.

SQL Injection در WordPress

در WordPress توسعه‌دهندگان معمولاً برای ارتباط با دیتابیس از کلاس $wpdb استفاده می‌کنند.

اگر مقدار کاربر وارد Query می‌شود، استفاده از $wpdb->prepare() اهمیت زیادی دارد.

نمونه:

<?php

global $wpdb;

$user_id = 123;

$query = $wpdb->prepare(
    "SELECT ID, user_login
     FROM {$wpdb->users}
     WHERE ID = %d",
    $user_id
);

$user = $wpdb->get_row($query);

در این مثال %d برای مقدار عددی استفاده شده است.

WordPress برای wpdb::prepare() Placeholderهای زیر را مستند کرده است:

%d = integer
%f = float
%s = string
%i = identifier

Placeholderها باید بدون کوتیشن داخل Query قرار بگیرند. همچنین %i از WordPress 6.2 برای Identifierهایی مانند نام جدول و ستون اضافه شده است.

استفاده از %i در WordPress

در WordPress 6.2 و نسخه‌های جدیدتر می‌توان برای Identifierها از %i استفاده کرد.

برای مثال:

global $wpdb;

$table = $wpdb->prefix . 'products';
$field = 'product_id';
$value = 123;

$query = $wpdb->prepare(
    'SELECT * FROM %i WHERE %i = %d',
    $table,
    $field,
    $value
);

$results = $wpdb->get_results($query);

در اینجا $table و $field به عنوان Identifier و $value به عنوان مقدار داده‌ای پردازش می‌شوند.

مستندات رسمی WordPress اضافه شدن %i را از نسخه 6.2 ثبت کرده‌اند. برای پروژه‌هایی که با نسخه‌های قدیمی WordPress سازگار هستند، باید این تفاوت نسخه‌ای در نظر گرفته شود.

یک اشتباه مهم در wpdb->prepare()

Placeholder را نباید داخل کوتیشن قرار داد.

صحیح:

$query = $wpdb->prepare(
    'SELECT * FROM users WHERE username = %s',
    $username
);

نادرست:

$query = $wpdb->prepare(
    "SELECT * FROM users WHERE username = '%s'",
    $username
);

مستندات WordPress تصریح می‌کنند که Placeholderهای wpdb::prepare() باید بدون کوتیشن در Query قرار بگیرند.

آیا mysqli_real_escape_string() برای جلوگیری از SQL Injection کافی است؟

خیر.

این یکی از مهم‌ترین نکاتی است که در آموزش SQL Injection باید به شکل دقیق توضیح داده شود.

تابع:

mysqli_real_escape_string()

برای Escape کردن رشته‌ها در شرایط مشخص کاربرد دارد، اما نباید راهکار اصلی طراحی Queryهای دارای ورودی کاربر محسوب شود.

OWASP Escape کردن تمام ورودی‌های کاربر را به عنوان راهکار اصلی توصیه نمی‌کند و آن را نسبت به Prepared Statement ضعیف‌تر و مستعد خطا می‌داند.

اولویت باید این باشد:

Prepared Statement
        ↓
Parameter Binding
        ↓
Input Validation
        ↓
Allowlist در موارد لازم
        ↓
Least Privilege

اصل Least Privilege در مقابله با SQL Injection

فرض کنید یک برنامه وب با یک حساب کاربری دیتابیس به MySQL متصل است.

اگر این حساب دسترسی کامل مدیریتی داشته باشد، یک آسیب‌پذیری در برنامه می‌تواند پیامد بسیار بزرگ‌تری ایجاد کند.

به همین دلیل باید حساب دیتابیس برنامه فقط دسترسی‌هایی را داشته باشد که واقعاً برای اجرای برنامه نیاز دارد.

برای نمونه، برنامه‌ای که فقط به خواندن داده نیاز دارد، نباید الزاماً مجوزهای مدیریتی یا دسترسی گسترده به تمام جداول داشته باشد.

OWASP توصیه می‌کند حساب‌های مورد استفاده برنامه‌ها حداقل سطح دسترسی لازم را داشته باشند و از اختصاص دسترسی DBA یا Administrator به حساب برنامه اجتناب شود.

مدیریت خطاهای SQL

پیام‌های خام دیتابیس نباید به کاربر نمایش داده شوند.

یک خطای SQL ممکن است اطلاعاتی درباره ساختار دیتابیس، نام جدول، ستون یا نوع DBMS افشا کند.

در محیط Production بهتر است جزئیات خطا در سیستم Logging ثبت شود و کاربر یک پیام عمومی دریافت کند.

برای مثال:

try {
    $stmt = $pdo->prepare(
        'SELECT id, username
         FROM users
         WHERE username = :username'
    );

    $stmt->execute([
        'username' => $username
    ]);
} catch (PDOException $e) {
    error_log($e->getMessage());

    http_response_code(500);
    exit('Database error');
}

در این ساختار، جزئیات فنی برای تیم توسعه ثبت می‌شود و مستقیماً به کاربر نمایش داده نمی‌شود.

SQL Injection در APIها

SQL Injection محدود به فرم‌های HTML نیست.

APIهایی که داده را از Query String، JSON، Header یا سایر منابع دریافت می‌کنند نیز باید همین اصول را رعایت کنند.

برای مثال:

GET /api/products?id=123

در سمت سرور نباید مقدار id مستقیماً داخل Query قرار بگیرد.

ساختار امن:

$id = filter_input(
    INPUT_GET,
    'id',
    FILTER_VALIDATE_INT
);

if ($id === false || $id === null) {
    http_response_code(400);
    exit('Invalid ID');
}

$stmt = $pdo->prepare(
    'SELECT id, name, price
     FROM products
     WHERE id = :id'
);

$stmt->execute([
    'id' => $id
]);

این رویکرد برای API، پنل مدیریت و صفحات معمول سایت یک اصل مشترک دارد. ورودی خارجی نباید مستقیماً وارد ساختار Query شود.

آیا ORM مانع SQL Injection می‌شود؟

ORMهایی مانند Eloquent، Hibernate، Entity Framework و Django ORM معمولاً امکان ساخت Queryهای پارامتری را فراهم می‌کنند.

اما ORM به خودی خود تضمین نمی‌کند که برنامه هیچ SQL Injection نداشته باشد.

مشکل زمانی می‌تواند ایجاد شود که توسعه‌دهنده از Raw SQL استفاده کند و ورودی کاربر را با اتصال رشته‌ای وارد آن کند.

بنابراین در پروژه‌های مبتنی بر ORM نیز باید Raw Queryها بررسی شوند.

قاعده ساده است:

هر جا SQL به صورت Dynamic ساخته می‌شود، مسیر ورود داده به Query باید بررسی شود.

تفاوت SQL Injection و XSS

SQL Injection و XSS هر دو می‌توانند از پردازش نادرست ورودی ناشی شوند، اما هدف و محل اجرای آن‌ها متفاوت است.

در SQL Injection، هدف اصلی منطق Query و پایگاه داده است.

در XSS، مهاجم تلاش می‌کند محتوای مخرب را به Contextی برساند که مرورگر آن را به عنوان HTML یا JavaScript پردازش کند.

بنابراین راهکارهای دفاعی این دو نیز یکسان نیستند.

Prepared Statement برای SQL Injection اهمیت دارد، در حالی که Output Encoding و کنترل Context برای جلوگیری از بسیاری از سناریوهای XSS اهمیت دارند.

آیا WAF جلوی SQL Injection را می‌گیرد؟

WAF یا Web Application Firewall می‌تواند برخی الگوهای شناخته‌شده حملات را شناسایی و مسدود کند.

اما WAF نباید جایگزین اصلاح کد شود.

معماری دفاعی مناسب بهتر است چند لایه داشته باشد:

Client
   ↓
WAF
   ↓
Web Server
   ↓
Application
   ↓
Input Validation
   ↓
Parameterized Query
   ↓
Database

اگر مهاجم بتواند یک Rule مربوط به WAF را دور بزند، کنترل‌های امنیتی داخل برنامه همچنان باید از دیتابیس محافظت کنند.

چگونه یک کد را از نظر SQL Injection بررسی کنیم؟

در Code Review باید مسیر داده از ورودی کاربر تا دیتابیس دنبال شود.

مثلاً این کد نیازمند بررسی امنیتی است:

$id = $_GET['id'];

$sql = "SELECT * FROM products WHERE id = " . $id;

$result = mysqli_query($connection, $sql);

مسیر داده چنین است:

$_GET['id']
      ↓
$id
      ↓
String Concatenation
      ↓
SQL Query
      ↓
Database

مشکل در همین نقطه ایجاد شده است.

نسخه بهتر:

$id = filter_input(
    INPUT_GET,
    'id',
    FILTER_VALIDATE_INT
);

if ($id === false || $id === null) {
    http_response_code(400);
    exit('Invalid ID');
}

$stmt = $mysqli->prepare(
    'SELECT id, name, price
     FROM products
     WHERE id = ?'
);

$stmt->bind_param('i', $id);
$stmt->execute();

در این نسخه، Validation انجام شده و Query نیز با Prepared Statement اجرا می‌شود.

SQL Injection و CI/CD

امنیت SQL Injection نباید فقط در مرحله نهایی تست شود.

در پروژه‌های حرفه‌ای بهتر است کنترل‌های امنیتی در چرخه توسعه قرار بگیرند.

Secure Coding
      ↓
Code Review
      ↓
Static Analysis
      ↓
Automated Tests
      ↓
Dependency Scanning
      ↓
DAST
      ↓
Production Monitoring

هدف این است که آسیب‌پذیری هرچه زودتر در چرخه توسعه شناسایی شود.

برای تیم‌های توسعه، بررسی Raw SQL، تست مسیرهای ورودی و کنترل Queryهای Dynamic اهمیت ویژه‌ای دارد.

چک‌لیست جلوگیری از SQL Injection

برای بررسی امنیت یک پروژه می‌توان این موارد را کنترل کرد:

  • استفاده از Prepared Statements برای Queryهای دارای داده خارجی
  • عدم اتصال مستقیم ورودی کاربر به رشته SQL
  • استفاده از Validation در سمت سرور
  • استفاده از Allowlist برای Identifierها و گزینه‌های محدود
  • بررسی Raw Queryهای موجود در پروژه
  • محدود کردن دسترسی حساب دیتابیس
  • عدم استفاده از حساب DBA برای اتصال برنامه
  • عدم نمایش خطاهای خام دیتابیس
  • ثبت خطاهای فنی در Log
  • بررسی APIها در کنار صفحات وب
  • بازبینی Queryهای Dynamic در Code Review
  • به‌روزرسانی Framework و Dependencyها
  • انجام تست امنیتی فقط روی محیط‌هایی که مجوز ارزیابی آن‌ها را دارید

سوالات متداول

SQL Injection چیست؟

SQL Injection آسیب‌پذیری‌ای است که در اثر ساخت ناامن Query و ترکیب داده کنترل‌نشده با دستور SQL ایجاد می‌شود. مهاجم در شرایط آسیب‌پذیر می‌تواند روی منطق Query اثر بگذارد.

بهترین روش جلوگیری از SQL Injection چیست؟

استفاده از Prepared Statements یا Parameterized Queries مهم‌ترین راهکار است. Validation، Allowlist و Least Privilege نیز به عنوان لایه‌های دفاعی مکمل استفاده می‌شوند.

آیا Validation به تنهایی کافی است؟

خیر. Validation باید در کنار Parameterized Query استفاده شود. OWASP نیز هشدار می‌دهد که داده اعتبارسنجی‌شده لزوماً برای قرار گرفتن مستقیم در یک Query ساخته‌شده با String Concatenation امن نیست.

آیا Escape کردن ورودی روش مناسبی است؟

Escape کردن در برخی شرایط کاربرد دارد، اما نباید روش اصلی مقابله با SQL Injection باشد. OWASP آن را نسبت به Prepared Statement راهکاری ضعیف‌تر و آخرین گزینه دفاعی می‌داند.

آیا PDO از SQL Injection جلوگیری می‌کند؟

PDO به خودی خود تضمین‌کننده امنیت نیست. استفاده صحیح از prepare() و execute() با پارامترهای جداشده از Query می‌تواند از SQL Injection جلوگیری کند.

آیا MySQLi برای جلوگیری از SQL Injection مناسب است؟

بله. MySQLi از Prepared Statements و Parameter Binding پشتیبانی می‌کند. الگوی prepare()، bind_param() و execute() روش استاندارد استفاده از این قابلیت است.

آیا WordPress در برابر SQL Injection ایمن است؟

WordPress APIهایی مانند $wpdb->prepare() را برای ساخت Queryهای پارامتری در اختیار توسعه‌دهندگان قرار می‌دهد، اما افزونه‌ها و قالب‌های دارای Queryهای سفارشی همچنان باید به شکل صحیح از این API استفاده کنند.

آیا %i در WordPress برای نام جدول استفاده می‌شود؟

بله. wpdb::prepare() از WordPress 6.2 از %i برای Identifierهایی مانند نام جدول و ستون پشتیبانی می‌کند.

آیا WAF جایگزین Prepared Statement است؟

خیر. WAF یک لایه دفاعی اضافی است. آسیب‌پذیری موجود در کد باید در خود برنامه اصلاح شود.

آیا ORMها SQL Injection را کاملاً حذف می‌کنند؟

خیر. ORMها بسیاری از Queryها را به شکل امن تولید می‌کنند، اما استفاده نادرست از Raw SQL یا Queryهای Dynamic همچنان می‌تواند آسیب‌پذیری ایجاد کند.

مهم‌ترین اصل امنیتی در برابر SQL Injection چیست؟

مرز بین کد SQL و داده کاربر باید حفظ شود. Prepared Statement این جداسازی را برای مقادیر Query فراهم می‌کند و در بخش‌هایی که Parameterization ممکن نیست، باید از طراحی مناسب یا Allowlist استفاده شود.

دیدگاهتان را بنویسید!

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

طرح پردازان مگین
سبد خرید
empty basket

هیچ محصولی در سبد خرید نیست.