الفكرة في سطر
الـ database مش بتعرف أنهي جزء من الـ query إنت كاتبه وأنهي جزء المستخدم كاتبه. لو لزقت الـ input جوه الـ string، المستخدم يقدر يكتب SQL مش اسم.
الكود اللي فيه الثغرة
// ❌ الـ input اتلزق في الـ queryapp.get("/users", async (req, res) => { const name = req.query.name; const sql = `SELECT * FROM users WHERE name = '${name}'`; const { rows } = await pool.query(sql); res.json(rows);});لو name قيمته ' OR '1'='1، الـ query بتبقى:
SELECT * FROM users WHERE name = '' OR '1'='1'
الشرط بقى صح دايمًا، فرجعت كل المستخدمين. ونفس الفكرة ممكن تتمد لقراية جداول تانية أو تعديل داتا، على حسب صلاحيات الـ database user.
الحل: parameterized queries
الـ input داتا، عمره ما يبقى جزء من نص الـ SQL. بتبعت الـ query بـ placeholders، والقيم لوحدها، والـ database نفسها هي اللي بتفصل بينهم.
app.get("/users", async (req, res) => { const sql = `SELECT * FROM users WHERE name = '${req.query.name}'`; const sql = "SELECT * FROM users WHERE name = $1"; const { rows } = await pool.query(sql, [req.query.name]); res.json(rows);});دلوقتي لو حد بعت ' OR '1'='1، الـ database هتدوّر على مستخدم اسمه حرفيًا كده، ومش هتلاقي.
الـ ORM مش بيحميك في كل حتة
Prisma و TypeORM و Sequelize بيعملوا parameters لوحدهم في الـ API العادي. المشكلة في الـ raw queries:
// ✅ tagged template: Prisma بيحوّل ${} لـ parametersawait prisma.$queryRaw`SELECT * FROM "User" WHERE email = ${email}`;// ❌ Unsafe: إنت اللي بتبني الـ stringawait prisma.$queryRawUnsafe(`SELECT * FROM "User" WHERE email = '${email}'`);const SORTS = { newest: "created_at DESC", name: "name ASC" };const orderBy = SORTS[req.query.sort] ?? SORTS.newest;const sql = `SELECT * FROM jobs ORDER BY ${orderBy} LIMIT $1`;طبقات زيادة
الـ database user بتاع التطبيق ميبقاش owner ولا superuser. يقرا ويكتب في الجداول اللي محتاجها بس.
zod أو class-validator على كل input: نوعه وطوله وشكله. مش بديل عن الـ parameters، بس بيقلل السطح.