سی ام اس نویسسی ام اس نویسیاد بگیر، سیستم خودتو بنویس
مجلهتماس با ما
سی ام اس نویسسی ام اس نویس

CMS نویس مرجعی برای آموزش برنامه نویسی وب و PHP، توسعه افزونه‌های اختصاصی وردپرس، طراحی ورک‌فلو و اتوماسیون با n8n و توسعه CMS های اختصاصی است. هدف ما ارائه آموزش‌های کاربردی و راهکارهای حرفه‌ای برای توسعه‌دهندگان، کسب‌وکارها و علاقه‌مندان به دنیای برنامه‌نویسی است.

پیوندهای سریع
محصولات فیزیکیمحصولات دیجیتالدوره‌های آموزشی
سی ام اس نویس
مجله سی ام اس نویستماس با ماپنل کاربری
سی ام اس نویس — تمامی حقوق برای سی ام اس نویس محفوظ است. © 1406 تماس با ما09307711941
SQL Injection در PHP چیست؟ آموزش جلوگیری از SQL Injection با PDO

SQL Injection در PHP چیست؟ آموزش جلوگیری از SQL Injection با PDO

ممدیر کل سیستم ۲۶ شهریور ۱۴۰۵ ۱۹ دقیقه مطالعه ۲ بازدید
SQL Injection در PHP چیست؟ آموزش جلوگیری از SQL Injection با PDO

SQL Injection یکی از شناخته‌شده‌ترین آسیب‌پذیری‌های امنیتی در برنامه‌های تحت وب است که زمانی اتفاق می‌افتد که ورودی کاربر بدون کنترل مناسب وارد Query دیتابیس شود.

اگر در یک برنامه PHP اطلاعاتی مانند نام کاربری، ایمیل، شناسه محصول یا عبارت جستجو را مستقیماً داخل Query قرار دهیم، ممکن است کاربر بتواند ساختار Query را تغییر دهد.

در این مقاله ابتدا بررسی می‌کنیم SQL Injection در PHP چیست و چرا ایجاد می‌شود، سپس با مثال‌های عملی تفاوت Query ناامن و Query امن را بررسی کرده و یاد می‌گیریم چگونه با PDO و Prepared Statements از این آسیب‌پذیری جلوگیری کنیم.


SQL Injection چیست؟

SQL Injection یا به اختصار SQLi نوعی آسیب‌پذیری است که در آن ورودی کنترل‌نشده کاربر به شکلی وارد دستور SQL می‌شود که بتواند منطق Query را تغییر دهد.

برای مثال فرض کنید یک برنامه PHP اطلاعات یک کاربر را بر اساس username دریافت می‌کند.

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


$username = $_GET['username'];



$sql = "SELECT * FROM users WHERE username = '$username'";

در ظاهر این کد کاملاً ساده است، اما مشکل اینجاست که مقدار $username مستقیماً وارد Query شده است.

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


SELECT * FROM users WHERE username = 'saeed'

اما وقتی ورودی کاربر بتواند بخشی از ساختار SQL را تحت تأثیر قرار دهد، رفتار Query می‌تواند تغییر کند.

نکته مهم: مشکل اصلی استفاده از PHP یا MySQL نیست؛ مشکل، ترکیب مستقیم داده‌ای است که از کاربر دریافت شده با کد SQL است.


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

برای درک بهتر، این Query را در نظر بگیرید:


$username = $_GET['username'];



$sql = "SELECT * FROM users WHERE username = '$username'";

در اینجا دو چیز با یکدیگر ترکیب شده‌اند:

  1. کد SQL
  2. داده‌ای که کاربر ارسال کرده است

در معماری ناامن، دیتابیس ممکن است نتواند به‌درستی تشخیص دهد کدام بخش Query، کد SQL و کدام بخش، داده کاربر است.

به همین دلیل باید کاری کنیم که مقدار کاربر به‌عنوان داده در نظر گرفته شود، نه بخشی از دستور SQL.

اینجاست که Prepared Statements وارد می‌شوند.


# یک مثال ساده از Query آسیب‌پذیر در PHP

فرض کنید جدولی به نام users داریم:


CREATE TABLE users (

    id INT PRIMARY KEY AUTO_INCREMENT,

    username VARCHAR(100),

    email VARCHAR(150),

    password VARCHAR(255)

);

حالا می‌خواهیم کاربر را بر اساس نام کاربری پیدا کنیم.

کد ناامن:


$username = $_GET['username'];



$sql = "SELECT * FROM users WHERE username = '$username'";



$result = $pdo->query($sql);

مشکل این کد این است که مقدار username مستقیماً داخل SQL قرار گرفته است.

مشکل کجاست؟

این قسمت:


'$username'

باعث می‌شود داده‌ای که کاربر ارسال کرده مستقیماً در ساختار Query قرار بگیرد.

بنابراین نباید به این شکل Query را بسازیم.


# روش اشتباه برای جلوگیری از SQL Injection

گاهی تصور می‌شود با حذف چند کاراکتر یا Escape کردن ورودی می‌توان مشکل را کاملاً حل کرد.

برای مثال:


$username = addslashes($_GET['username']);

و سپس:


$sql = "SELECT * FROM users WHERE username = '$username'";

این روش راهکار اصلی و قابل اتکایی برای جلوگیری از SQL Injection نیست.

همچنین استفاده از روش‌هایی مانند:


htmlspecialchars()

برای جلوگیری از SQL Injection صحیح نیست.

htmlspecialchars() بیشتر برای جلوگیری از برخی حملات XSS هنگام نمایش داده در HTML استفاده می‌شود.

بنابراین نباید ابزارهای مربوط به امنیت HTML را با ابزارهای امنیت SQL اشتباه بگیریم.


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

یکی از روش‌های استاندارد و مهم برای جلوگیری از SQL Injection استفاده از:

Prepared Statements

است.

در PHP می‌توانیم این کار را با PDO انجام دهیم.

به‌جای اینکه مقدار کاربر را مستقیماً داخل Query قرار دهیم، Query را به شکل زیر می‌نویسیم:


$sql = "SELECT * FROM users WHERE username = :username";

سپس مقدار واقعی را جداگانه به Query می‌دهیم.


# PDO چیست؟

PDO مخفف PHP Data Objects است و یک رابط استاندارد در PHP برای کار با دیتابیس‌ها محسوب می‌شود.

با PDO می‌توانیم با دیتابیس‌هایی مانند MySQL کار کنیم و از قابلیت‌هایی مانند Prepared Statements استفاده کنیم.

یک اتصال ساده به MySQL:


$pdo = new PDO(

    'mysql:host=localhost;dbname=test;charset=utf8mb4',

    'root',

    ''

);

اما بهتر است تنظیمات خطا و حالت دریافت داده را نیز مشخص کنیم:


$pdo = new PDO(

    'mysql:host=localhost;dbname=test;charset=utf8mb4',

    'root',

    '',

    [

        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,

        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,

        PDO::ATTR_EMULATE_PREPARES => false,

    ]

);

در اینجا:

PDO::ATTR_ERRMODE

باعث می‌شود خطاهای PDO به شکل Exception مدیریت شوند.

PDO::ATTR_DEFAULT_FETCH_MODE

مشخص می‌کند داده‌های دریافت‌شده به‌صورت پیش‌فرض با چه ساختاری برگردند.

در این مثال:


PDO::FETCH_ASSOC

استفاده شده است.

PDO::ATTR_EMULATE_PREPARES

مشخص می‌کند PDO از Prepared Statements شبیه‌سازی‌شده استفاده کند یا درایور دیتابیس بتواند از Prepared Statements واقعی استفاده کند.

برای MySQL می‌توان این گزینه را false قرار داد.


# Prepared Statement در PHP چیست؟

Prepared Statement به زبان ساده یعنی:

> ساختار SQL را از داده‌ای که کاربر وارد می‌کند جدا کنیم.

مثلاً به‌جای این:


$username = $_GET['username'];



$sql = "SELECT * FROM users WHERE username = '$username'";

از این استفاده می‌کنیم:


$username = $_GET['username'];



$sql = "SELECT * FROM users WHERE username = :username";



$stmt = $pdo->prepare($sql);



$stmt->execute([

    'username' => $username

]);

در اینجا:


:username

یک Placeholder است.

مقدار واقعی username جداگانه به Query داده می‌شود.

این تفاوت بسیار مهم است.


# استفاده از Named Parameters در PDO

یکی از روش‌های خوانا برای Prepared Statements استفاده از پارامترهای نام‌گذاری‌شده است.

مثلاً:


$sql = "

    SELECT *

    FROM users

    WHERE username = :username

";



$stmt = $pdo->prepare($sql);



$stmt->execute([

    'username' => $username

]);



$user = $stmt->fetch();

در اینجا:


:username

پارامتر Query است و:


[

    'username' => $username

]

مقدار آن را مشخص می‌کند.


# استفاده از پارامترهای موقعیتی در PDO

PDO از Placeholderهای موقعیتی نیز پشتیبانی می‌کند.

مثلاً:


$sql = "

    SELECT *

    FROM users

    WHERE username = ?

";



$stmt = $pdo->prepare($sql);



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

علامت:


?

به‌عنوان Placeholder استفاده شده است.

برای Queryهای ساده، هر دو روش قابل استفاده هستند.

در پروژه‌های بزرگ‌تر، Placeholderهای نام‌گذاری‌شده معمولاً خوانایی بیشتری دارند.


# استفاده از Prepared Statement برای SELECT

فرض کنید می‌خواهیم کاربری را با ایمیل پیدا کنیم:


$email = $_POST['email'];



$sql = "

    SELECT id, username, email

    FROM users

    WHERE email = :email

";



$stmt = $pdo->prepare($sql);



$stmt->execute([

    'email' => $email

]);



$user = $stmt->fetch();

در این حالت مقدار ایمیل مستقیماً به Query اضافه نشده است.


# استفاده از Prepared Statement برای INSERT

Prepared Statements فقط برای SELECT نیستند.

می‌توانیم برای درج اطلاعات نیز از آنها استفاده کنیم:


$username = $_POST['username'];

$email = $_POST['email'];



$sql = "

    INSERT INTO users (username, email)

    VALUES (:username, :email)

";



$stmt = $pdo->prepare($sql);



$stmt->execute([

    'username' => $username,

    'email' => $email

]);

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


# استفاده از Prepared Statement برای UPDATE

برای بروزرسانی اطلاعات:


$id = $_POST['id'];

$email = $_POST['email'];



$sql = "

    UPDATE users

    SET email = :email

    WHERE id = :id

";



$stmt = $pdo->prepare($sql);



$stmt->execute([

    'email' => $email,

    'id' => $id

]);


# استفاده از Prepared Statement برای DELETE

برای حذف اطلاعات نیز می‌توان از همین روش استفاده کرد:


$id = $_POST['id'];



$sql = "

    DELETE FROM users

    WHERE id = :id

";



$stmt = $pdo->prepare($sql);



$stmt->execute([

    'id' => $id

]);


# آیا استفاده از Prepared Statements به تنهایی کافی است؟

Prepared Statements مهم‌ترین دفاع در برابر SQL Injection در Queryهای دارای داده‌های متغیر هستند، اما امنیت یک برنامه فقط به آن محدود نمی‌شود.

در یک پروژه واقعی بهتر است علاوه بر Prepared Statements موارد زیر را نیز رعایت کنیم:

  • اعتبارسنجی ورودی‌ها
  • محدود کردن سطح دسترسی کاربر دیتابیس
  • عدم نمایش خطاهای حساس دیتابیس به کاربر
  • مدیریت صحیح Exceptionها
  • به‌روزرسانی PHP و کتابخانه‌ها
  • استفاده از HTTPS
  • کنترل دسترسی کاربران
  • ثبت رویدادهای امنیتی مهم

بنابراین Prepared Statements را باید بخشی از معماری امنیتی برنامه بدانیم، نه تمام آن.


# تفاوت Validation و Prepared Statement

این دو موضوع کاربرد یکسانی ندارند.

فرض کنید کاربر باید یک شناسه عددی ارسال کند:


$id = $_GET['id'];

می‌توانیم نوع آن را بررسی کنیم:


if (!filter_var($id, FILTER_VALIDATE_INT)) {

    exit('Invalid ID');

}

اما حتی بعد از Validation بهتر است Query را به شکل امن بنویسیم:


$stmt = $pdo->prepare(

    "SELECT * FROM products WHERE id = :id"

);



$stmt->execute([

    'id' => $id

]);

به بیان ساده:

Validation بررسی می‌کند داده برای منطق برنامه مناسب است یا خیر.

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

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


# جلوگیری از SQL Injection در جستجوی LIKE

یکی از موارد رایج، جستجوی محصولات است.

مثلاً:


$search = $_GET['search'];

Query امن:


$sql = "

    SELECT *

    FROM products

    WHERE name LIKE :search

";



$stmt = $pdo->prepare($sql);



$stmt->execute([

    'search' => '%' . $search . '%'

]);



$products = $stmt->fetchAll();

در اینجا مقدار % را در مقدار پارامتر قرار داده‌ایم:


'%' . $search . '%'

نه اینکه مقدار را مستقیماً داخل SQL قرار دهیم.


# استفاده از IN با Prepared Statements

فرض کنید چند شناسه محصول داریم:


$ids = [10, 20, 30];

نمی‌توانیم به این شکل عمل کنیم:


$sql = "SELECT * FROM products WHERE id IN ($ids)";

هم از نظر فنی صحیح نیست و هم روش مناسبی برای ساخت Query نیست.

برای تعداد متغیر پارامترها باید Placeholderهای مناسب ایجاد کنیم.

مثلاً:


$ids = [10, 20, 30];



$placeholders = implode(',', array_fill(0, count($ids), '?'));



$sql = "

    SELECT *

    FROM products

    WHERE id IN ($placeholders)

";



$stmt = $pdo->prepare($sql);

$stmt->execute($ids);



$products = $stmt->fetchAll();

در نهایت Query از نظر Placeholder چیزی شبیه این خواهد بود:


SELECT *

FROM products

WHERE id IN (?, ?, ?)

و مقادیر به‌صورت جداگانه ارسال می‌شوند.


# آیا Placeholder برای نام ستون کار می‌کند؟

یک اشتباه رایج این است که تصور کنیم می‌توانیم نام ستون را نیز با Placeholder ارسال کنیم.

مثلاً:


$column = $_GET['column'];



$sql = "

    SELECT *

    FROM products

    ORDER BY :column

";

این روش راهکار صحیحی برای انتخاب نام ستون نیست.

Placeholderها برای مقادیر هستند، نه نام جدول‌ها و ستون‌ها.

اگر کاربر بتواند ترتیب نمایش را انتخاب کند، باید گزینه‌های مجاز را مشخص کنیم.

مثلاً:


$allowedSorts = [

    'name',

    'price',

    'created_at'

];



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



if (!in_array($sort, $allowedSorts, true)) {

    $sort = 'created_at';

}

سپس:


$sql = "

    SELECT *

    FROM products

    ORDER BY $sort

";

در اینجا مقدار $sort از یک لیست مشخص و محدود انتخاب شده است.

این تکنیک را معمولاً Whitelist می‌نامیم.


# LIMIT و OFFSET در Queryهای داینامیک

برای Pagination نیز باید دقت کنیم.

مثلاً:


$limit = $_GET['limit'];

$offset = $_GET['offset'];

بهتر است ابتدا نوع آنها را کنترل کنیم:


$limit = filter_var(

    $_GET['limit'] ?? 10,

    FILTER_VALIDATE_INT

);



$offset = filter_var(

    $_GET['offset'] ?? 0,

    FILTER_VALIDATE_INT

);



$limit = $limit !== false ? max(1, min($limit, 100)) : 10;

$offset = $offset !== false ? max(0, $offset) : 0;

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

نکته مهم این است که برای بخش‌هایی مثل نام ستون و ساختار Query نمی‌توان صرفاً از Placeholder انتظار داشت؛ این قسمت‌ها نیازمند Validation و Whitelist هستند.


# bindValue در PDO چیست؟

روش دیگری برای مقداردهی پارامترها استفاده از bindValue() است:


$sql = "

    SELECT *

    FROM users

    WHERE id = :id

";



$stmt = $pdo->prepare($sql);



$stmt->bindValue(':id', $id, PDO::PARAM_INT);



$stmt->execute();

در اینجا نوع مقدار را نیز مشخص کرده‌ایم:


PDO::PARAM_INT

برای رشته:


PDO::PARAM_STR

استفاده می‌شود.


# تفاوت bindValue و bindParam چیست؟

این دو متد شبیه یکدیگر هستند اما دقیقاً یکسان نیستند.

bindValue() مقدار فعلی متغیر را Bind می‌کند:


$stmt->bindValue(':id', $id, PDO::PARAM_INT);

اما bindParam() پارامتر را به خود متغیر Bind می‌کند و رفتار آن با متغیر متفاوت است:


$stmt->bindParam(':id', $id, PDO::PARAM_INT);

برای اکثر Queryهای معمول، استفاده از:


$stmt->execute([

    'id' => $id

]);

ساده‌تر و خواناتر است.


# SQL Injection در صفحه Login

یکی از مکان‌هایی که SQL Injection می‌تواند بسیار حساس باشد، سیستم ورود کاربران است.

فرض کنید کد زیر را داریم:


$username = $_POST['username'];

$password = $_POST['password'];



$sql = "

    SELECT *

    FROM users

    WHERE username = '$username'

    AND password = '$password'

";

این طراحی چند مشکل دارد.

اول اینکه داده‌ها مستقیماً داخل Query قرار گرفته‌اند.

دوم اینکه ذخیره و مقایسه مستقیم Password به این شکل یک روش مناسب برای نگهداری رمز عبور نیست.

روش بهتر این است که ابتدا کاربر را با Query امن پیدا کنیم:


$username = $_POST['username'];



$stmt = $pdo->prepare("

    SELECT id, username, password

    FROM users

    WHERE username = :username

");



$stmt->execute([

    'username' => $username

]);



$user = $stmt->fetch();

سپس رمز عبور را با:


password_verify()

بررسی کنیم:


if ($user && password_verify($password, $user['password'])) {

    // Login successful

}

رمز عبور نیز باید هنگام ثبت‌نام با password_hash() ذخیره شده باشد:


$hash = password_hash($password, PASSWORD_DEFAULT);


# آیا Escape کردن ورودی جای Prepared Statement را می‌گیرد؟

بهتر است برای Queryهای داینامیک به جای تکیه بر Escape کردن دستی، از Prepared Statements استفاده کنیم.

این روش:


$username = addslashes($username);

جایگزین مناسبی برای Prepared Statements نیست.

همچنین:


htmlspecialchars()

برای SQL Injection طراحی نشده است.

قاعده ساده:

برای SQL از Prepared Statements استفاده کنید.

برای خروجی HTML، Context مناسب خروجی را در نظر بگیرید.

این دو مسئله امنیتی متفاوت هستند.


# SQL Injection در Laravel

اگر بعداً از PHP خام وارد Laravel شوید، معمولاً با Query Builder و Eloquent بسیاری از Queryها به شکل امن‌تری نوشته می‌شوند.

برای مثال:


$user = DB::table('users')

    ->where('email', $email)

    ->first();

یا:


$user = User::where('email', $email)->first();

در این حالت Framework برای مقداردهی Query از مکانیزم Binding استفاده می‌کند.

اما این به معنی مصونیت کامل برنامه Laravel از SQL Injection نیست.

مثلاً وقتی SQL خام و ورودی کاربر را با روش نامناسب ترکیب می‌کنیم، همچنان باید مراقب باشیم.

بنابراین استفاده از Laravel نیز جایگزین درک SQL Injection و اصول Query امن نیست.


# آیا ORMها کاملاً جلوی SQL Injection را می‌گیرند؟

خیر.

ORM و Query Builder می‌توانند بسیاری از Queryها را با Binding امن اجرا کنند، اما توسعه‌دهنده همچنان مسئول نحوه ساخت Query است.

در هر Framework باید مراقب قسمت‌هایی مانند:

  • Raw SQL
  • نام ستون‌ها
  • نام جدول‌ها
  • ORDER BY داینامیک
  • Queryهای سفارشی
  • ساخت Query با String

باشیم.

اصل مهم این است:

> داده کاربر نباید بدون کنترل وارد ساختار SQL شود.


# Second-Order SQL Injection چیست؟

همه SQL Injectionها الزاماً در همان لحظه‌ای که داده وارد سیستم می‌شود اتفاق نمی‌افتند.

در Second-Order SQL Injection ممکن است داده‌ای ابتدا در دیتابیس ذخیره شود و بعداً در قسمت دیگری از برنامه بدون رعایت اصول امنیتی وارد یک Query شود.

برای مثال:


ورودی کاربر

      ↓

ذخیره در دیتابیس

      ↓

استفاده در بخش دیگری از برنامه

      ↓

ساخت Query ناامن

به همین دلیل صرفاً نباید هنگام دریافت اطلاعات به امنیت آن فکر کنیم.

هر زمان داده‌ای قرار است وارد Query شود، نحوه استفاده از آن باید امن باشد.


# آیا Prepared Statement برای همه چیز کافی است؟

خیر.

برای مثال، اگر کاربر بتواند نام ستون را انتخاب کند، نمی‌توانیم صرفاً آن نام را با Placeholder جایگزین کنیم.

در این شرایط باید از Whitelist استفاده کنیم:


$allowedColumns = [

    'name',

    'price',

    'created_at'

];



$column = $_GET['sort'] ?? 'created_at';



if (!in_array($column, $allowedColumns, true)) {

    $column = 'created_at';

}

بنابراین می‌توان یک قانون کاربردی داشت:

برای Values

از Prepared Statements استفاده کنید.

برای Identifiers

مانند نام جدول و ستون، از Whitelist و Validation استفاده کنید.


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

اشتباه اول: استفاده از String Concatenation


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

اگر داده کنترل‌نشده باشد، روش مناسبی نیست.


اشتباه دوم: استفاده از htmlspecialchars برای SQL


$value = htmlspecialchars($value);

این تابع راهکار جلوگیری از SQL Injection نیست.


اشتباه سوم: استفاده از addslashes به‌عنوان راهکار اصلی


$value = addslashes($value);

این روش نباید جایگزین Prepared Statements شود.


اشتباه چهارم: اعتماد به Validation به‌عنوان تنها دفاع

حتی اگر مقدار ورودی را Validation کنیم، برای Queryهای دارای مقدار متغیر همچنان بهتر است از Prepared Statements استفاده کنیم.


اشتباه پنجم: تصور امنیت کامل به دلیل استفاده از ORM

استفاده از ORM به‌تنهایی تضمین نمی‌کند تمام Queryهای برنامه امن باشند.


اشتباه ششم: نمایش خطای دیتابیس به کاربر

در محیط Production نباید جزئیات داخلی دیتابیس را به کاربر نمایش دهیم.

به‌جای:


echo $e->getMessage();

در محیط Production بهتر است خطا را Log کنیم و پیام عمومی نمایش دهیم:


try {

    // Database operation

} catch (PDOException $e) {

    error_log($e->getMessage());



    echo "خطایی در پردازش درخواست رخ داده است.";

}


# یک نمونه کامل و امن با PDO

در این مثال می‌خواهیم کاربر را با نام کاربری جستجو کنیم.


<?php



$pdo = new PDO(

    'mysql:host=localhost;dbname=test;charset=utf8mb4',

    'root',

    '',

    [

        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,

        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,

        PDO::ATTR_EMULATE_PREPARES => false,

    ]

);



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



if ($username === '') {

    exit('نام کاربری را وارد کنید.');

}



$stmt = $pdo->prepare("

    SELECT id, username, email

    FROM users

    WHERE username = :username

");



$stmt->execute([

    'username' => $username

]);



$user = $stmt->fetch();



if ($user) {

    echo "Username: " . htmlspecialchars($user['username']);

} else {

    echo "کاربر پیدا نشد.";

}

در این مثال چند نکته مهم را همزمان رعایت کرده‌ایم:

  • اتصال به دیتابیس با PDO
  • استفاده از Exception برای خطاها
  • استفاده از Prepared Statement
  • جدا کردن داده کاربر از Query
  • بررسی ورودی
  • Escape کردن خروجی HTML

توجه کنید که htmlspecialchars() در اینجا برای نمایش مقدار در HTML استفاده شده است، نه برای جلوگیری از SQL Injection.


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

قبل از انتشار یک برنامه PHP، موارد زیر را بررسی کنید:

  • [ ] Queryهای داینامیک با String Concatenation ساخته نشده‌اند.
  • [ ] برای مقادیر از Prepared Statements استفاده شده است.
  • [ ] از PDO یا روش امن مشابه استفاده شده است.
  • [ ] ورودی‌های کاربر Validation می‌شوند.
  • [ ] برای نام ستون و جدول از Whitelist استفاده شده است.
  • [ ] از htmlspecialchars() به‌عنوان راهکار SQL Injection استفاده نشده است.
  • [ ] از addslashes() به‌عنوان راهکار اصلی امنیت دیتابیس استفاده نشده است.
  • [ ] خطاهای دیتابیس در Production به کاربر نمایش داده نمی‌شوند.
  • [ ] کاربر دیتابیس حداقل دسترسی موردنیاز را دارد.
  • [ ] رمزهای عبور با password_hash() ذخیره می‌شوند.
  • [ ] رمزهای عبور با password_verify() بررسی می‌شوند.
  • [ ] Queryهای Raw در Frameworkها بررسی شده‌اند.
  • [ ] قسمت‌های داینامیک مانند ORDER BY و نام ستون‌ها Whitelist شده‌اند.

# جمع‌بندی

SQL Injection زمانی ایجاد می‌شود که داده‌ای که تحت کنترل کاربر است به شکل ناامن وارد ساختار Query SQL شود.

یکی از مهم‌ترین راهکارهای جلوگیری از این آسیب‌پذیری در PHP استفاده از PDO و Prepared Statements است.

به‌جای:


$sql = "SELECT * FROM users WHERE username = '$username'";

از:


$stmt = $pdo->prepare(

    "SELECT * FROM users WHERE username = :username"

);



$stmt->execute([

    'username' => $username

]);

استفاده کنید.

در کنار Prepared Statements باید Validation، مدیریت خطا، محدود کردن دسترسی دیتابیس، امنیت Passwordها و کنترل قسمت‌های داینامیک Query را نیز رعایت کنید.

اگر در حال یادگیری PHP هستید، درک SQL Injection فقط برای امنیت PHP اهمیت ندارد؛ این مفهوم هنگام کار با MySQL، PDO، Laravel، WordPress، REST API و بسیاری از Frameworkها و سیستم‌های وب نیز بسیار مهم است.


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

SQL Injection در PHP چیست؟

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

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

برای مقادیر متغیر داخل Query، استفاده از Prepared Statements با PDO یکی از روش‌های اصلی و استاندارد است.

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

PDO به‌خودی‌خود تضمین امنیت نمی‌دهد؛ اما استفاده صحیح از Prepared Statements در PDO می‌تواند از SQL Injection در Queryهای دارای پارامتر جلوگیری کند.

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

خیر. htmlspecialchars() برای ایمن‌سازی خروجی در HTML کاربرد دارد و راهکار اصلی جلوگیری از SQL Injection نیست.

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

خیر. نباید addslashes() را جایگزین Prepared Statements کرد.

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

Laravel در Query Builder و Eloquent از Binding استفاده می‌کند و بسیاری از Queryها را به شکل امن اجرا می‌کند، اما استفاده نادرست از Raw SQL یا ساخت Queryهای داینامیک همچنان می‌تواند مشکل امنیتی ایجاد کند.

تفاوت SQL Injection و XSS چیست؟

SQL Injection معمولاً دیتابیس و Queryهای SQL را هدف قرار می‌دهد، در حالی که XSS به اجرای محتوای مخرب در Context مرورگر و صفحه وب مربوط است. روش‌های دفاعی این دو نیز متفاوت هستند.

نظرات (0)

نظرات پس از تأیید مدیر نمایش داده می‌شوند

هنوز نظری ثبت نشده است؛ اولین نفر باشید!

مقالات مرتبط

ساخت CMS با Laravel 12؛ آموزش ساخت سیستم مدیریت محتوا از صفر

ساخت CMS با Laravel 12؛ آموزش ساخت سیستم مدیریت محتوا از صفر

۲۴ دقیقه
ساخت فروشگاه اینترنتی با Laravel 12؛ آموزش پروژه‌محور از صفر تا طراحی هسته فروشگاه

ساخت فروشگاه اینترنتی با Laravel 12؛ آموزش پروژه‌محور از صفر تا طراحی هسته فروشگاه

۲۲ دقیقه
ساخت Marketplace با Laravel؛ آموزش طراحی فروشگاه چندفروشندگی

ساخت Marketplace با Laravel؛ آموزش طراحی فروشگاه چندفروشندگی

۲۲ دقیقه
مدیریت Queueهای سنگین در Laravel؛ آموزش پردازش کارهای طولانی

مدیریت Queueهای سنگین در Laravel؛ آموزش پردازش کارهای طولانی

۲۲ دقیقه