در سال‌های اول کارم با PHP، امنیت را مسئله‌ی کسی می‌دانستم که «سایت بزرگ» دارد. فکر می‌کردم کد من که ساده است، چرا کسی بخواهد به آن حمله کند؟ تا روزی که یک سایت کوچک مشتری، با کمتر از صد بازدید روزانه، توسط یک ربات خودکار اسکن شد و به یک صفحه‌ی شرط‌بندی تبدیل شد. آن روز فهمیدم که امنیت، مسئله‌ی «اندازه» نیست؛ مسئله‌ی «احتمال» است. ربات‌های خودکار، تمام وب را جارو می‌زنند و به هر سایتی که در هر خط کد آسیب‌پذیر باشد، حمله می‌کنند. از آن روز، امنیت را به‌عنوان بخشی از هر خط کد، درونی کردم — نه به‌عنوان یک مرحله‌ی جداگانه در پایان پروژه.

این مقاله، همان مسیری است که در سال‌های بعد طی کردم تا از یک توسعه‌دهنده‌ای که «امنیت را به بعد موکول می‌کند» به یک توسعه‌دهنده‌ای که «امنیت را از روز اول می‌نویسد» تبدیل شوم. ده لایه‌ی اصلی امنیت در PHP را باز می‌کنم — از ورودی کاربر تا نشست، از دیتابیس تا فایل‌ها — با نمونه‌های عملی و اشتباهات رایجی که در پروژه‌های واقعی دیده‌ام. اگر با مبانی PHP آشنایی ندارید، پیشنهاد می‌کنم پیش از این مقاله، آموزش PHP از صفر برای مبتدیان را بخوانید. برای درک عمیق‌تر مفاهیم امنیتی، امنیت وب چیست و چه اصولی دارد مکمل دقیق این مقاله است.

امنیت، یک ذهنیت است، نه یک مرحله

در تجربه‌ی من، بزرگ‌ترین اشتباه در پروژه‌های واقعی این است که امنیت را به‌عنوان یک «مرحله» می‌بینند — مرحله‌ای که در پایان پروژه، پیش از انتشار، به آن می‌پردازند. این رویکرد، از دو جهت اشتباه است. اول، چون در آن مرحله، تغییرات ساختاری بسیار پرهزینه‌تر از روز اول است. دوم، چون خیلی از آسیب‌پذیری‌ها در سطح معماری کد هستند، نه در جزئیات. یعنی اگر معماری را بدون امنیت طراحی کنید، در پایان نمی‌توانید آن را فقط با چند خط به کد امن تبدیل کنید.

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

  • امنیت از روز اول: هر تابع، هر کلاس، هر endpoint از همان ابتدا با اصول امنیتی نوشته می‌شود.
  • دفاع در عمق: هیچ لایه‌ی امنیتی، کافی نیست. باید چند لایه وجود داشته باشد تا اگر یکی شکست خورد، بقیه جلوی حمله را بگیرند.
  • هرگز اعتماد نکن: هیچ داده‌ای — نه ورودی کاربر، نه داده‌ی دیتابیس، نه خروجی API — نباید بدون اعتبارسنجی و پاک‌سازی استفاده شود.

این سه اصل، شالوده‌ی همه‌ی لایه‌هایی است که در ادامه باز می‌کنم. تجربه‌ی من: توسعه‌دهندگانی که این ذهنیت را درونی می‌کنند، حتی بدون اینکه همه‌ی تکنیک‌های امنیتی را بلد باشند، کدشان امن‌تر از کسانی است که ده تکنیک را حفظ کرده‌اند اما ذهنیت امنیتی ندارند.

امنیت، مثل بهداشت است: نمی‌توانید ماهی یک بار دوش بگیرید و انتظار داشته باشید سالم بمانید. باید بخشی از روتین روزانه‌تان باشد.

لایه‌ی اول: هرگز به ورودی کاربر اعتماد نکنید

اولین لایه‌ی امنیت، و شاید مهم‌ترین لایه، این است: هر داده‌ای که از بیرون می‌آید، بالقوه خطرناک است. این داده‌ها شامل $_GET، $_POST، $_COOKIE، $_FILES، و حتی داده‌هایی است که از API های بیرونی می‌آید. هیچ‌کدام از این‌ها را نباید بی‌اعتبارسنجی استفاده کنید.

سه سطح اعتبارسنجی که در پروژه‌های خودم رعایت می‌کنم:

سطح اول — اعتبارسنجی نوع: قبل از هر چیز، مطمئن شوید که داده از نوع انتظار است. مثلاً اگر انتظار یک عدد دارید، مطمئن شوید که عدد است:

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

if ( $id === false || $id === null ) {
    http_response_code( 400 );
    exit( 'شناسه نامعتبر است.' );
}

سطح دوم — اعتبارسنجی محدوده: حتی اگر داده از نوع درست باشد، باید محدوده‌اش هم بررسی شود. مثلاً اگر $id یک عدد مثبت است، باید بزرگ‌تر از صفر باشد:

if ( $id <= 0 || $id > 999999 ) {
    http_response_code( 400 );
    exit( 'شناسه در محدوده‌ی مجاز نیست.' );
}

سطح سوم — اعتبارسنجی محتوا: برای رشته‌ها، باید محتوا را بررسی کنید. مثلاً اگر انتظار یک ایمیل دارید، از فیلتر FILTER_VALIDATE_EMAIL استفاده کنید:

$email = filter_input( INPUT_POST, 'email', FILTER_VALIDATE_EMAIL );

if ( ! $email ) {
    http_response_code( 400 );
    exit( 'ایمیل نامعتبر است.' );
}

یک نکته‌ی حیاتی: اعتبارسنجی سمت کلاینت (با JavaScript) کافی نیست. هر کسی می‌تواند با ابزارهایی مثل Postman یا curl، داده‌های دلخواه به سرور بفرستد و اعتبارسنجی سمت کلاینت را دور بزند. اعتبارسنجی سمت سرور، تنها لایه‌ی قابل اعتماد است. اگر با آموزش جاوااسکریپت آشنا نیستید، آموزش JavaScript از صفر تصویر کامل‌تری از این تفکیک ارائه می‌دهد.

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

لایه‌ی دوم: SQL Injection و Prepared Statements

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

سه لایه‌ی دفاعی که در کد خودم رعایت می‌کنم:

لایه‌ی اول — همیشه Prepared Statements: تمام کوئری‌هایی که ورودی کاربر دارند، باید از Prepared Statements استفاده کنند. با PDO:

$stmt = $pdo->prepare( 'SELECT * FROM users WHERE email = :email' );
$stmt->execute( [ ':email' => $email ] );
$user = $stmt->fetch();

در این کد، ورودی $email به‌عنوان «داده» پردازش می‌شود، نه به‌عنوان «کد». این تفاوت، تفاوت بین یک کد امن و یک کد آسیب‌پذیر است. مسیر کامل کار با PDO را در آموزش PDO در PHP و اتصال PHP به MySQL به تفصیل باز کرده‌ام.

لایه‌ی دوم — محدودسازی دسترسی کاربر دیتابیس: کاربر دیتابیس شما نباید دسترسی DROP یا ALTER داشته باشد، مگر اینکه واقعا لازم باشد. اگر سایت شما فقط به SELECT، INSERT، UPDATE و DELETE نیاز دارد، همین‌ها را بدهید و بس.

لایه‌ی سوم — خطاهای عمومی: هرگز پیام خطای دقیق دیتابیس را به کاربر نشان ندهید. این پیام‌ها می‌توانند شامل نام جدول، نام ستون، و حتی ساختار دیتابیس شما باشند. به‌جای آن، خطا را در لاگ ثبت کنید و پیام عمومی نشان دهید. برای مطالعه‌ی بیشتر درباره‌ی این حمله، SQL Injection چیست و چگونه از آن جلوگیری کنیم را ببینید.

لایه‌ی سوم: XSS و پاک‌سازی خروجی

XSS (Cross-Site Scripting) یکی دیگر از آسیب‌پذیری‌های رایج است. در این حمله، مهاجم کد جاوااسکریپت خودش را از طریق ورودی کاربر به صفحه تزریق می‌کند. وقتی کاربر دیگری صفحه را باز می‌کند، کد مهاجم اجرا می‌شود و می‌تواند کوکی‌های کاربر را بدزدد، اطلاعات حساس را بفرستد، یا کاربر را به سایت‌های دیگر هدایت کند.

دفاع در برابر XSS، در دو سطح انجام می‌شود:

سطح اول — پاک‌سازی خروجی: هر داده‌ای که از دیتابیس یا ورودی کاربر می‌آید و در HTML نمایش داده می‌شود، باید پاک‌سازی شود. تابع اصلی برای این کار htmlspecialchars() است:

$safe_name = htmlspecialchars( $user_name, ENT_QUOTES, 'UTF-8' );
echo 'سلام، ' . $safe_name;

پارامتر ENT_QUOTES باعث می‌شود که هم کوتیشن تکی و هم دوتایی کدگذاری شوند. همیشه UTF-8 را به‌عنوان کاراکترست مشخص کنید.

سطح دوم — کدگذاری زمینه‌ای: اگر داده را در زمینه‌ی متفاوت (مثل داخل یک href یا داخل یک تگ style) قرار می‌دهید، باید از توابع مناسب استفاده کنید:

// برای URL
$url = 'https://example.com/?q=' . urlencode( $query );

// برای JavaScript
$js_value = json_encode( $value, JSON_HEX_TAG | JSON_HEX_AMP );

// برای صفت HTML
$attr = htmlspecialchars( $value, ENT_QUOTES, 'UTF-8' );

یک قاعده‌ی طلایی که در پروژه‌های خودم رعایت می‌کنم: در PHP، هر چیزی که به HTML می‌رود، باید پاک‌سازی شود. حتی اگر داده از دیتابیس خودتان آمده، چون می‌تواند توسط حمله‌ای دیگر (مثل SQL Injection) آلوده شده باشد.

در XSS، مهاجم از «اعتماد بی‌جای شما به ورودی کاربر» استفاده می‌کند. دفاع در برابر آن، از «بی‌اعتمادی سیستماتیک شما به هر داده» شروع می‌شود.

لایه‌ی چهارم: CSRF و توکن‌های امنیتی

CSRF (Cross-Site Request Forgery) حمله‌ای است که در آن، مهاجم کاربر را فریب می‌دهد تا یک درخواست ناخواسته به سایت شما بفرستد. مثلاً کاربری که در سایت شما لاگین است، بدون اینکه بداند، روی لینکی کلیک می‌کند که یک محصول را از حسابش خریداری می‌کند یا رمز عبورش را تغییر می‌دهد.

دفاع در برابر CSRF، با توکن‌های امنیتی (CSRF Token) انجام می‌شود. ایده‌ی ساده: در هر فرم، یک توکن یگانه و تصادفی قرار می‌دهید. سرور، آن توکن را در درخواست بررسی می‌کند. مهاجم که از سایت شما توکن ندارد، نمی‌تواند درخواست معتبر بسازد.

پیاده‌سازی CSRF Token در PHP:

// تولید توکن در نشست
function generate_csrf_token(): string {
    if ( empty( $_SESSION['csrf_token'] ) ) {
        $_SESSION['csrf_token'] = bin2hex( random_bytes( 32 ) );
    }
    return $_SESSION['csrf_token'];
}

// در فرم
echo '<input type="hidden" name="csrf_token" value="' . generate_csrf_token() . '">';

// در بررسی درخواست
function verify_csrf_token( string $token ): bool {
    if ( empty( $_SESSION['csrf_token'] ) ) {
        return false;
    }
    return hash_equals( $_SESSION['csrf_token'], $token );
}

if ( ! verify_csrf_token( $_POST['csrf_token'] ?? '' ) ) {
    http_response_code( 403 );
    exit( 'درخواست نامعتبر است.' );
}

سه نکته‌ی مهم در این کد:

  1. استفاده از random_bytes(): تابع random_bytes() یک رشته‌ی تصادفی امن تولید می‌کند. هرگز از rand() یا mt_rand() برای توکن امنیتی استفاده نکنید؛ این توابع قابل پیش‌بینی هستند.
  2. استفاده از hash_equals(): این تابع، مقایسه‌ای مقاوم در برابر حمله‌ی زمان‌سنجی (Timing Attack) فراهم می‌کند. مقایسه‌ی مستقیم با == می‌تواند نشت اطلاعاتی داشته باشد.
  3. تولید توکن در هر نشست: توکن CSRF باید در هر نشست (Session) جدید تولید شود و با بسته شدن نشست، از بین برود.

موضوع مشابهی را در بستر وردپرس هم به عنوان یکی از پایه‌های امنیت فرم‌ها مطرح کرده‌ام — وردپرس توابع داخلی برای CSRF دارد که در هوک‌های وردپرس و افزایش امنیت کد به آن اشاره کرده‌ام.

لایه‌ی پنجم: مدیریت امن رمز عبور

مدیریت رمز عبور، یکی از پرتکرارترین اشتباهات در پروژه‌های واقعی است. سه اصل مهم:

اصل اول — هرگز رمز را به‌صورت متن ساده ذخیره نکنید: این جمله را باید به‌عنوان یک قانون مطلق در نظر بگیرید. همیشه رمز عبور را با یک الگوریتم رمزنگاری قوی (Hashing) ذخیره کنید. در PHP، تابع password_hash() استاندارد است:

$hashed = password_hash( $password, PASSWORD_DEFAULT );

// ذخیره‌ی $hashed در دیتابیس

تابع password_hash() به‌طور پیش‌فرض از الگوریتم bcrypt استفاده می‌کند که یکی از امن‌ترین الگوریتم‌های موجود است.

اصل دوم — بررسی رمز با password_verify(): برای بررسی صحت رمز، همیشه از password_verify() استفاده کنید، نه مقایسه‌ی مستقیم هش:

if ( password_verify( $input_password, $stored_hash ) ) {
    // ورود موفق
} else {
    // رمز نادرست
}

اصل سوم — ارتقای الگوریتم: با گذشت زمان، الگوریتم‌های رمزنگاری می‌توانند ضعیف شوند. تابع password_needs_rehash() به شما می‌گوید که آیا هش ذخیره‌شده، نیاز به به‌روزرسانی دارد یا نه:

if ( password_needs_rehash( $stored_hash, PASSWORD_DEFAULT ) ) {
    $new_hash = password_hash( $input_password, PASSWORD_DEFAULT );
    // به‌روزرسانی $new_hash در دیتابیس
}

سه نکته‌ی مکمل در مدیریت رمز عبور:

  • پیچیدگی رمز: حداقل ۸ کاراکتر، ترکیبی از حروف بزرگ و کوچک، اعداد و علائم. پیچیدگی بیشتر، رمز را در برابر حمله Brute Force مقاوم‌تر می‌کند.
  • محدودسازی تلاش‌های ورود: بعد از چند تلاش ناموفق، کاربر باید موقتا مسدود شود. این لایه، در برابر حملات Brute Force حیاتی است. موضوع مشابه در بستر وردپرس در جلوگیری از حملات Brute Force در وردپرس آمده است.
  • دو‌عاملی (2FA): برای حساب‌های حساس (مثل ادمین)، فعال‌سازی دو‌عاملی را جدی بگیرید. این لایه، حتی اگر رمز لو برود، دسترسی را مسدود می‌کند.

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

نشست (Session) و کوکی، دو ابزار اصلی برای حفظ وضعیت کاربر در PHP هستند. اگر این‌ها امن نباشند، مهاجم می‌تواند نشست کاربر را بدزدد یا جعل کند. سه تنظیم حیاتی:

تنظیم اول — session.cookie_httponly: این تنظیم باعث می‌شود که کوکی نشست فقط از طریق HTTP قابل دسترسی باشد و نه از طریق JavaScript. این یک لایه‌ی دفاعی در برابر XSS است:

ini_set( 'session.cookie_httponly', '1' );
session_start();

تنظیم دوم — session.cookie_secure: اگر سایت شما روی HTTPS است، این تنظیم باعث می‌شود که کوکی فقط از طریق HTTPS ارسال شود:

ini_set( 'session.cookie_secure', '1' );
session_start();

تنظیم سوم — session.cookie_samesite: این تنظیم، یک لایه‌ی دفاعی در برابر CSRF فراهم می‌کند. مقدار Lax یا Strict:

ini_set( 'session.cookie_samesite', 'Lax' );
session_start();

علاوه بر این سه تنظیم، دو عادت مهم:

  • بازتولید شناسه‌ی نشست در ورود: وقتی کاربر وارد می‌شود، شناسه‌ی نشست باید بازتولید شود تا در برابر حمله‌ی Session Fixation مقاوم باشد:
session_regenerate_id( true );
  • زمان انقضای نشست: نشست باید بعد از مدت مشخصی (مثلا ۳۰ دقیقه بی‌فعالیتی) منقضی شود. این کار، خطر استفاده‌ی ناخواسته از نشست باز مانده را کم می‌کند.

یک نکته‌ی ظریف: در محیط‌های اشتراکی، ذخیره‌ی نشست‌ها در فایل می‌تواند خطرناک باشد. در آن حالت، استفاده از Redis یا Memcached برای ذخیره‌ی نشست‌ها توصیه می‌شود.

لایه‌ی هفتم: آپلود و مدیریت فایل

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

لایه‌ی اول — بررسی نوع فایل با MIME Type واقعی: هرگز به پسوند فایل اعتماد نکنید. از تابع finfo_file() برای تشخیص MIME Type واقعی استفاده کنید:

$finfo = finfo_open( FILEINFO_MIME_TYPE );
$mime = finfo_file( $finfo, $_FILES['file']['tmp_name'] );
finfo_close( $finfo );

$allowed = [ 'image/jpeg', 'image/png', 'image/webp' ];

if ( ! in_array( $mime, $allowed, true ) ) {
    exit( 'نوع فایل مجاز نیست.' );
}

لایه‌ی دوم — محدودسازی اندازه: حداکثر اندازه‌ی فایل را مشخص کنید و آن را بررسی کنید:

$max_size = 5 * 1024 * 1024;

if ( $_FILES['file']['size'] > $max_size ) {
    exit( 'فایل بزرگ‌تر از حد مجاز است.' );
}

لایه‌ی سوم — ذخیره در مسیر غیرقابل اجرا: فایل‌های آپلودی را در مسیری ذخیره کنید که سرور، کد PHP را در آن اجرا نمی‌کند. در Apache، این کار با یک فایل .htaccess در پوشه‌ی uploads انجام می‌شود:

php_flag engine off
<FilesMatch "\.(php|phtml|php3|php4|php5|phps|pl|py|jsp|asp|sh|cgi)$">
    Require all denied
</FilesMatch>

لایه‌ی چهارم — نام‌گذاری مجدد فایل: هرگز نام اصلی فایل را نگه ندارید. یک نام یگانه با پسوند امن تولید کنید:

$safe_name = bin2hex( random_bytes( 16 ) ) . '.' . $extension;

لایه‌ی پنجم — بررسی محتوای فایل تصویری: برای فایل‌های تصویری، از getimagesize() استفاده کنید تا مطمئن شوید که فایل واقعا تصویر است، نه یک کد مخرب با پسوند تصویر:

$image_info = getimagesize( $_FILES['file']['tmp_name'] );

if ( $image_info === false ) {
    exit( 'فایل معتبر تصویری نیست.' );
}

موضوعات مرتبط در بستر وردپرس را در امنیت آپلود فایل در وردپرس به تفصیل باز کرده‌ام.

لایه‌ی هشتم: حمله‌ی File Inclusion

در PHP، دو تابع include و require برای اضافه کردن فایل‌های دیگر استفاده می‌شوند. اگر مسیر این فایل‌ها از ورودی کاربر بیاید، مهاجم می‌تواند فایل‌های حساس سرور را بخواند یا کد مخرب خودش را اجرا کند. سه لایه‌ی دفاعی:

لایه‌ی اول — فهرست سفید مسیرها: هرگز مسیر مستقیم از ورودی کاربر را به include ندهید. از یک فهرست سفید استفاده کنید:

$pages = [
    'home'     => 'pages/home.php'،
    'about'    => 'pages/about.php'،
    'contact'  => 'pages/contact.php'،
];

$requested = $_GET['page'] ?? 'home';

if ( ! isset( $pages[ $requested ] ) ) {
    exit( 'صفحه یافت نشد.' );
}

include $pages[ $requested ];

لایه‌ی دوم — غیرفعال کردن allow_url_include: در فایل php.ini، این تنظیم باید روی Off باشد. با این کار، PHP نمی‌تواند فایل‌ها را از URL های دور (مثل http://example.com/malicious.php) بارگذاری کند:

allow_url_include = Off
allow_url_fopen = Off

لایه‌ی سوم — بررسی مسیر با realpath(): اگر مجبورید مسیر را از ورودی کاربر بگیرید، حتما آن را با realpath() بررسی کنید:

$base = '/var/www/html/includes/';
$file = realpath( $base . basename( $_GET['file'] ) );

if ( $file === false || strpos( $file, $base ) !== 0 ) {
    exit( 'فایل نامعتبر است.' );
}

include $file;

در این کد، basename() تمام مسیرهای نسبی را حذف می‌کند و realpath() مطمئن می‌شود که فایل، در مسیر مجاز قرار دارد.

لایه‌ی نهم: مدیریت امن خطاها

مدیریت خطا در PHP، در سطح امنیتی دو چهره دارد: از یک سو، خطاها ابزار دیباگ هستند و باید در محیط توسعه دیده شوند؛ از سوی دیگر، در محیط تولید، هر خطای خام می‌تواند اطلاعات حساس را افشا کند. سه اصل در این لایه:

اصل اول — تفکیک محیط توسعه از تولید: در محیط توسعه، display_errors را روشن و error_reporting را روی E_ALL بگذارید. در محیط تولید، display_errors را خاموش کنید اما log_errors را روشن نگه دارید:

// در محیط تولید
error_reporting( E_ALL );
ini_set( 'display_errors', '0' );
ini_set( 'log_errors', '1' );
ini_set( 'error_log', '/path/to/error.log' );

اصل دوم — پیام‌های عمومی برای کاربر: هرگز پیام دقیق خطا را به کاربر ندهید. به‌جای «خطا در اتصال به دیتابیس shop_db با کاربر root»، بگویید «سرویس موقتا در دسترس نیست. لطفا بعدا تلاش کنید.».

اصل سوم — هندلر سراسری: از سه هندلر set_error_handler، set_exception_handler و register_shutdown_function استفاده کنید تا هیچ خطای مدیریت‌نشده‌ای در پروژه باقی نماند:

set_exception_handler( function ( Throwable $e ) {
    error_log( 'Uncaught: ' . $e->getMessage() );
    http_response_code( 500 );
    echo 'خطای غیرمنتظره. تیم فنی در جریان است.';
} );

مسیر کامل مدیریت خطا در PHP را در مدیریت خطا در PHP به تفصیل باز کرده‌ام. در بستر وردپرس، این موضوع را هم در راهنمای جامع رفع خطاهای رایج وردپرس آورده‌ام.

لایه‌ی دهم: وابستگی‌ها و Composer

اگر پروژه‌ی شما از Composer استفاده می‌کند، وابستگی‌ها هم بخشی از سطح حمله‌ی شما هستند. یک آسیب‌پذیری در یک کتابخانه‌ی جانبی، می‌تواند کل سیستم را در معرض خطر قرار دهد. سه عمل که در پروژه‌های خودم اجرا می‌کنم:

عمل اول — بررسی دوره‌ای آسیب‌پذیری‌ها: دستور زیر، آسیب‌پذیری‌های شناخته‌شده در وابستگی‌ها را گزارش می‌کند:

composer audit

این دستور باید در CI/CD یا به‌صورت دوره‌ای اجرا شود. تجربه‌ی من: در پروژه‌های بلندمدت، ماهی یک بار اجرای این دستور، جلوی چند فاجعه‌ی احتمالی را گرفته است. اگر با Composer آشنایی ندارید، آموزش Composer در PHP نقطه‌ی شروع دقیقی است.

عمل دوم — به‌روزرسانی دوره‌ای: وابستگی‌ها را با احتیاط به‌روز کنید. به‌روزرسانی می‌تواند آسیب‌پذیری‌ها را رفع کند، اما می‌تواند تعارض هم ایجاد کند. راه‌حل: در محیط استیجینگ اول تست کنید، بعد در تولید.

عمل سوم — قفل کردن نسخه‌ها: فایل composer.lock را در مخزن نگه دارید. این فایل، نسخه‌های دقیق وابستگی‌ها را قفل می‌کند و باعث می‌شود که در محیط‌های مختلف، همان نسخه‌ها نصب شوند.

موضوع مشابه در بستر وردپرس را در دانلود افزونه مطمئن وردپرس به تفصیل باز کرده‌ام — اصل یکسان است: هر کتابخانه‌ی جانبی، یک نقطه‌ی ورود بالقوه برای حمله است.

لایه‌ی یازدهم: هدرهای امنیتی HTTP

هدرهای امنیتی HTTP، یک لایه‌ی دفاعی هستند که از سمت سرور ارسال می‌شوند و به مرورگر دستور می‌دهند که چگونه با سایت شما رفتار کند. پنج هدر اصلی که در پروژه‌های خودم فعال می‌کنم:

هدرکارکردمقدار توصیه‌شده
X-Content-Type-Optionsجلوگیری از تشخیص MIME خودکارnosniff
X-Frame-Optionsجلوگیری از ClickjackingSAMEORIGIN
Content-Security-Policyمحدودسازی منابع قابل بارگذاریسیاست‌های مختلف
Strict-Transport-Securityاجبار HTTPSmax-age=31536000
Referrer-Policyمحدودسازی ارسال Referrerstrict-origin-when-cross-origin

پیاده‌سازی این هدرها در PHP:

header( 'X-Content-Type-Options: nosniff' );
header( 'X-Frame-Options: SAMEORIGIN' );
header( 'Referrer-Policy: strict-origin-when-cross-origin' );
header( 'Strict-Transport-Security: max-age=31536000; includeSubDomains' );
header( 'Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'' );

نکته‌ی مهم: هدر Content-Security-Policy بسیار قدرتمند است اما تنظیمش حساس است. یک سیاست اشتباه می‌تواند سایت شما را از کار بیندازد. پیشنهاد من: ابتدا با حالت Content-Security-Policy-Report-Only شروع کنید و گزارش‌ها را ببینید، سپس سیاست نهایی را اعمال کنید.

عادت‌های امنیتی روزانه

بعد از تسلط بر یازده لایه‌ی امنیتی، تفاوت بین یک توسعه‌دهنده‌ی متوسط و حرفه‌ای، در عادت‌های روزانه است. هفت عادتی که در پروژه‌های خودم به آن‌ها پایبندم:

  • هرگز به ورودی کاربر اعتماد نکنید: حتی اگر از یک منبع قابل اعتماد بیاید. اعتبارسنجی، پاک‌سازی و کدگذاری خروجی، سه گام همیشگی.
  • Prepared Statements را برای همه‌چیز استفاده کنید: نه فقط SELECT، نه فقط INSERT — همه‌ی کوئری‌ها.
  • هرگز رمز را در متن کد نگذارید: از متغیر محیطی یا فایل تنظیمات جداگانه استفاده کنید.
  • در محیط تولید، خطاها را نمایش ندهید: فقط لاگ کنید و پیام عمومی نشان دهید.
  • کوکی‌ها را با httponly و secure تنظیم کنید: این دو، لایه‌ی پایه‌ی امنیت نشست هستند.
  • وابستگی‌ها را دوره‌ای به‌روز کنید: دستور composer audit را ماهی یک بار اجرا کنید.
  • هدرهای امنیتی HTTP را فعال کنید: حتی اگر به نظر بی‌اهمیت باشند، لایه‌ی دفاعی مهمی هستند.

یک عادت مهم که در پروژه‌های خودم به آن پایبندم: هر بار که یک آسیب‌پذیری جدید در دنیای PHP کشف می‌شود، آن را بررسی می‌کنم و می‌بینم آیا پروژه‌های من در معرض هستند یا نه. این رویکرد، از «واکنش پس از حادثه» به «پیشگیری پیش از حادثه» تبدیل می‌شود. منابع معتبر برای پیگیری اخبار امنیتی PHP را در بهترین منابع یادگیری PHP در سال ۲۰۲۶ آورده‌ام.

امنیت، یک محصول نیست؛ یک فرآیند است. هیچ‌وقت «تمام» نمی‌شود؛ فقط «به‌روز» می‌ماند یا «کهنه» می‌شود. تفاوت این دو، تفاوت بین یک پروژه‌ی سالم و یک پروژه‌ی آلوده است.

نقشه‌ی ادامه‌ی مسیر

بعد از تسلط بر این یازده لایه، سه مسیر اصلی برای ادامه وجود دارد:

مسیر اول — تست امنیتی: ابزارهایی مثل OWASP ZAP و Burp Suite Community Edition، به شما کمک می‌کنند که سایت خود را از دید یک مهاجم بررسی کنید. شروع این مسیر نیازمند درک عمیق مفاهیم امنیتی است — همان چیزی که در امنیت وب چیست و چه اصولی دارد باز کرده‌ام. اگر با آسیب‌پذیری‌های رایج آشنا نیستید، آسیب‌پذیری وب چیست و انواع آسیب‌پذیری‌های رایج وب دو منبع کلیدی این مسیر هستند.

مسیر دوم — استانداردهای امنیتی: OWASP Top 10 یک فهرست مرجع از ده آسیب‌پذیری اصلی در اپلیکیشن‌های وب است. آشنایی با این فهرست، شما را با نقشه‌ی ذهنی امنیتی جامعه‌ی جهانی هم‌راستا می‌کند.

مسیر سوم — ابزارهای تحلیل ایستا: ابزارهایی مثل PHPStan، Psalm و Rector، کد شما را قبل از اجرا تحلیل می‌کنند و آسیب‌پذیری‌های پنهان را کشف می‌کنند. تجربه‌ی من: در پروژه‌های بزرگ، این ابزارها تفاوت بین یک کد با امنیت متوسط و یک کد با امنیت حرفه‌ای را می‌سازند.

در هر سه مسیر، یک توصیه‌ی مشترک دارم: درک امنیت را با یک پروژه‌ی واقعی بسازید. یک پروژه‌ی کوچک انتخاب کنید و آن را با رعایت این یازده لایه بنویسید. بعد از اتمام، از یک ابزار اسکن امنیتی استفاده کنید و ببینید چه آسیب‌پذیری‌هایی کشف می‌شود. تجربه‌ی من: هر آسیب‌پذیری که کشف می‌کنید، یک درس ارزشمند است که در پروژه‌ی بعدی، خودش را نشان می‌دهد.

و حالا یک تمرین ساده که می‌تواند در همین امشب، مسیر حرفه‌ای‌شدن‌تان را شروع کند: یک فرم ثبت‌نام ساده بسازید که از پنج لایه‌ی امنیتی استفاده کند — اعتبارسنجی ورودی، Prepared Statement برای درج، password_hash() برای رمز، CSRF Token در فرم، و پاک‌سازی خروجی. این فرم، شاید هفتاد خط بیشتر نباشد، اما اگر از فردا هر پروژه‌ای را با همین پنج لایه شروع کنید، شش ماه بعد متوجه می‌شوید که پروژه‌های‌تان، در برابر تمام حمله‌های خودکارِ دنیای وب، چند برابر مقاوم‌تر از قبل شده‌اند. همین هفتاد خط، در طول چند سال، چندین فاجعه‌ی احتمالی را از سایت‌های شما دور می‌کند.

اگر در مسیر، به یک رفتار غیرمنتظره رسیدید — مثلا اینکه چرا password_verify() در برابر حمله‌ی زمان‌سنجی مقاوم است، یا چرا یک آسیب‌پذیری که فکر می‌کردید بسته شده، از یک مسیر جانبی باز می‌شود — همان مشاهدات را در دیدگاه‌ها بنویسید. امنیت، از آن دسته موضوعاتی است که هر تجربه‌ی عملی، یک درس تازه در خودش دارد؛ و آن درس وقتی با جامعه به اشتراک گذاشته شود، چند برابر می‌شود. اگر در بستر وردپرس کار می‌کنید و با یک آسیب‌پذیری خاص روبه‌رو شده‌اید، بهترین افزونه‌های امنیتی وردپرس و امنیت وردپرس چیست مسیر بعدی شما هستند. 🔐