فكِّك هجمات XSS وCSRF وحقن SQL لتفهم بالضبط ما الذي تمنعه فعليًا دوال مثل htmlspecialchars والرموز المميّزة والعبارات المُجهَّزة.
افتح هذا الدرس في Kodokonتشترك العائلات الثلاث الكبرى لثغرات الويب - XSS وCSRF وعمليات الحقن - في السبب الجذري نفسه: بيانات يوفّرها المستخدم ينتهي بها الأمر مُفسَّرةً على أنها كود (HTML أو SQL أو طلب مشروع). لنبدأ بـ XSS: إذا عرضت قيمة قادمة من $_GET دون تهريب، فيمكن لمهاجم أن يحقن وسم <script> وينفّذ JavaScript في متصفحات زوّارك الآخرين - فيسرق الجلسات وينفّذ إجراءات دون علمهم. مع ?comment=<script>alert(1)</script>، يُشغّل الإخراج الأول أدناه السكربت؛ أما الثاني فيُبطِله. هنا رايتان مهمّتان: تحوّل ENT_QUOTES علامات الاقتباس المفردة أيضًا (وإلا فإن سمة محدّدة بعلامات اقتباس مفردة تبقى قابلة للحقن)، وتستبدل ENT_SUBSTITUTE تسلسلات UTF-8 غير الصالحة بدل إرجاع سلسلة فارغة. ومنذ PHP 8.1، أصبحت الرايتان أخيرًا هما الوضع الافتراضي، لكنّ ذكرهما صراحةً يوثّق نيّتك ويحمي الكود الذي يعمل على إصدارات أقدم.
<?php
declare(strict_types=1);
$comment = $_GET['comment'] ?? '';
echo '<div>' . $comment . '</div>';
function e(string $value): string
{
return htmlspecialchars(
$value,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
}
echo '<div>' . e($comment) . '</div>';يستغل CSRF حقيقة بسيطة: يرفق المتصفح تلقائيًا كوكيز الجلسة بأيّ طلب موجَّه نحو نطاقك، حتى لو صدر من موقع طرف ثالث. لذا يمكن لنموذج خبيث أن يُرسِل الطلب /account/delete مستخدمًا جلسة الضحية. والدفاع الكلاسيكي هو الرمز المُتزامِن (synchronizer token): سرّ عشوائي مُخزَّن في الجلسة ومطلوب في كل طلب يُجري تعديلًا. ولا يستطيع موقع طرف ثالث قراءته ولا تخمينه، فلا يمكنه توفيره.
<?php
session_start();
if (!isset($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
$sent = $_POST['csrf'] ?? '';
$valid = is_string($sent)
&& hash_equals($_SESSION['csrf'], $sent);
$isPost = $_SERVER['REQUEST_METHOD'] === 'POST';
if ($isPost && !$valid) {
http_response_code(403);
exit('Invalid CSRF token');
}
?>
<form method="post">
<input type="hidden" name="csrf"
value="<?= htmlspecialchars($_SESSION['csrf']) ?>">
<button>Delete my account</button>
</form>يتّبع حقن SQL النمط نفسه: فدمج قيمة قادمة من المستخدم داخل استعلام يعني السماح للمستخدم بكتابة SQL - إذ يحوّل ' OR 1=1 -- مُرشِّحًا إلى الجدول بأكمله. والحل ليس التهريب بل العبارة المُجهَّزة: ينتقل نص الاستعلام (مع عناصره النائبة مثل :email) والقيمُ بشكل منفصل إلى المحرّك، الذي لا يمكنه بالتالي أن يخلط أبدًا بين البيانات والكود. تفصيل للخبراء: مع MySQL، تُحاكي PDO العبارات المُجهَّزة افتراضيًا، مع التهريب في جانب العميل؛ اضبط PDO::ATTR_EMULATE_PREPARES على false للحصول على عبارات مُجهَّزة أصيلة حقًا.
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
PDO::ATTR_ERRMODE,
PDO::ERRMODE_EXCEPTION
);
$pdo->exec(
'CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL
)'
);
$email = $_GET['email'] ?? '';
$stmt = $pdo->prepare(
'SELECT id FROM users WHERE email = :email'
);
$stmt->execute(['email' => $email]);
var_dump($stmt->fetch(PDO::FETCH_ASSOC));