From 2135edcf4345ea5aa9d97acd8336aa65e8518e4b Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sat, 25 Jul 2026 16:48:10 +0300 Subject: [PATCH] Close the remaining ciphertext joins and a SQL injection in email verification - Customer-service notes joined to the account by comparing encrypted phone columns. Both notes tables now carry phone_key, written when a note is saved, and the three joins match on it. - The email_verifications join was comparing a plaintext column against an encrypted one, so it never matched and `verified` was always NULL in both passenger and driver sign-in. It is now resolved in PHP against the decrypted address, which fixes a pre-existing bug rather than only preparing for GCM. - auth/sendVerifyEmail.php built all three of its statements by interpolating the request values into SQL. Any caller could inject through the email or token field. Now parameterised. - serviceapp/register.php duplicate detection consults the users indexes and writes them with the row. Sweep confirms no join or lookup compares two encrypted columns any more. Co-Authored-By: Claude Opus 5 --- backend/auth/driver/login.php | 31 +++++++++++++++++-- backend/auth/login.php | 29 +++++++++++++++-- backend/auth/sendVerifyEmail.php | 16 +++++----- backend/scripts/migrate.php | 7 +++++ backend/serviceapp/addNotesDriver.php | 7 +++-- .../serviceapp/driverWhoregisterFfterCall.php | 2 +- backend/serviceapp/getJsonFile.php | 2 +- backend/serviceapp/getdriverstotalMonthly.php | 4 +-- backend/serviceapp/register.php | 24 +++++++++++--- 9 files changed, 98 insertions(+), 24 deletions(-) diff --git a/backend/auth/driver/login.php b/backend/auth/driver/login.php index eb27381c..d948418b 100644 --- a/backend/auth/driver/login.php +++ b/backend/auth/driver/login.php @@ -25,13 +25,34 @@ $sql = "SELECT driver.maritalStatus, driver.created_at, driver.updated_at, - email_verifications.verified + driver.email AS _email_enc FROM driver -LEFT JOIN email_verifications ON email_verifications.email = driver.email WHERE driver.phone = :phone AND driver.email = :email"; + +/** + * حالة توثيق البريد. + * + * كان الاستعلام يربط email_verifications.email بعمود البريد في الحساب، لكن + * الأول يُخزَّن نصاً صريحاً والثاني مشفّراً — فالربط لم يكن يطابق شيئاً أصلاً + * وكانت verified تعود NULL دائماً. نجلبها هنا بالبريد الأصلي. + */ +function fetchEmailVerified(PDO $con, ?string $plainEmail): ?int +{ + if (!$plainEmail) return null; + try { + $st = $con->prepare("SELECT verified FROM email_verifications WHERE email = ? LIMIT 1"); + $st->execute([$plainEmail]); + $v = $st->fetchColumn(); + return $v === false ? null : (int) $v; + } catch (PDOException $e) { + error_log('[email_verifications] ' . $e->getMessage()); + return null; + } +} + $stmt = $con->prepare($sql); $stmt->bindParam(':email', $email); $stmt->bindParam(':phone', $phone); @@ -39,6 +60,12 @@ $stmt->execute(); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); $count = $stmt->rowCount(); +if ($count > 0) { + $plainEmail = $encryptionHelper->decryptData($data[0]['_email_enc'] ?? null) ?: null; + $data[0]['verified'] = fetchEmailVerified($con, $plainEmail); + unset($data[0]['_email_enc']); +} + if ($count > 0) { $stored_password = $data[0]['password']; if (password_verify($password, $stored_password)) { diff --git a/backend/auth/login.php b/backend/auth/login.php index 636f8253..79ead5ff 100644 --- a/backend/auth/login.php +++ b/backend/auth/login.php @@ -62,18 +62,43 @@ $sql = "SELECT passengers.`maritalStatus`, passengers.`created_at`, passengers.`updated_at`, - email_verifications.verified + passengers.`email` AS `_email_enc` FROM `passengers` -LEFT JOIN email_verifications ON email_verifications.email = passengers.email WHERE $where"; + +/** + * حالة توثيق البريد. + * + * كان الاستعلام يربط email_verifications.email بعمود البريد في الحساب، لكن + * الأول يُخزَّن نصاً صريحاً والثاني مشفّراً — فالربط لم يكن يطابق شيئاً أصلاً + * وكانت verified تعود NULL دائماً. نجلبها هنا بالبريد الأصلي. + */ +function fetchEmailVerified(PDO $con, ?string $plainEmail): ?int +{ + if (!$plainEmail) return null; + try { + $st = $con->prepare("SELECT verified FROM email_verifications WHERE email = ? LIMIT 1"); + $st->execute([$plainEmail]); + $v = $st->fetchColumn(); + return $v === false ? null : (int) $v; + } catch (PDOException $e) { + error_log('[email_verifications] ' . $e->getMessage()); + return null; + } +} + $stmt = $con->prepare($sql); $stmt->execute($params); $data = $stmt->fetchAll(PDO::FETCH_ASSOC); $count = $stmt->rowCount(); if ($count > 0) { + $plainEmail = $encryptionHelper->decryptData($data[0]['_email_enc'] ?? null) ?: null; + $data[0]['verified'] = fetchEmailVerified($con, $plainEmail); + unset($data[0]['_email_enc']); + $stored_password = $data[0]['password']; if (password_verify($password, $stored_password)) { unset($data[0]['password']); diff --git a/backend/auth/sendVerifyEmail.php b/backend/auth/sendVerifyEmail.php index d37aae3e..1bc4edcf 100644 --- a/backend/auth/sendVerifyEmail.php +++ b/backend/auth/sendVerifyEmail.php @@ -4,9 +4,8 @@ require_once __DIR__ . '/../connect.php'; $email = filterRequest("email"); $token = filterRequest("token"); -$sql = "SELECT * FROM `email_verifications` WHERE `email` = '$email'"; -$stmt = $con->prepare($sql); -$stmt->execute(); +$stmt = $con->prepare("SELECT * FROM `email_verifications` WHERE `email` = ?"); +$stmt->execute([$email]); $rowCount = $stmt->rowCount(); @@ -41,9 +40,9 @@ SEFER Team. if ($rowCount > 0) { // The email already exists, so update the data - $sql = "UPDATE `email_verifications` SET `token` = '$token' WHERE `email` = '$email'"; - $stmt = $con->prepare($sql); - $stmt->execute(); + // كانت القيم تُدمج في نص الاستعلام مباشرةً — حقن SQL عبر البريد أو الرمز. + $stmt = $con->prepare("UPDATE `email_verifications` SET `token` = ? WHERE `email` = ?"); + $stmt->execute([$token, $email]); if ($stmt->rowCount() > 0) { // The update was successful @@ -55,9 +54,8 @@ if ($rowCount > 0) { } } else { // The email does not exist, so insert the data - $sql = "INSERT INTO `email_verifications` (`email`, `token`) VALUES ('$email', '$token')"; - $stmt = $con->prepare($sql); - $stmt->execute(); + $stmt = $con->prepare("INSERT INTO `email_verifications` (`email`, `token`) VALUES (?, ?)"); + $stmt->execute([$email, $token]); if ($stmt->rowCount() > 0) { // The insertion was successful diff --git a/backend/scripts/migrate.php b/backend/scripts/migrate.php index 1538aeac..a436598b 100644 --- a/backend/scripts/migrate.php +++ b/backend/scripts/migrate.php @@ -89,6 +89,11 @@ $columns = [ ['driver', 'phone_key', "VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط جداول التحقق'"], ['passengers', 'phone_key', "VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط جداول التحقق'"], + // جداول ملاحظات خدمة العملاء تُربط بالسائق/الراكب عبر الهاتف المشفّر، + // وهو ربط يفشل لحظة أن يصبح التشفير عشوائياً. + ['notesForDriverService', 'phone_key', "VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط بالحساب'"], + ['notesForPassengerService', 'phone_key', "VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط بالحساب'"], + // بقية الجداول التي يُبحث فيها بحقل مشفّر ['users', 'email_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC لبريد موظف الخدمة'"], ['users', 'phone_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC لهاتف موظف الخدمة'"], @@ -114,6 +119,8 @@ $indexes = [ ['driver', 'idx_driver_national_bidx', 'national_bidx'], ['driver', 'idx_driver_phone_key', 'phone_key'], ['passengers', 'idx_passengers_phone_key', 'phone_key'], + ['notesForDriverService', 'idx_notes_driver_phone_key', 'phone_key'], + ['notesForPassengerService', 'idx_notes_passenger_phone_key', 'phone_key'], ]; $applied = 0; diff --git a/backend/serviceapp/addNotesDriver.php b/backend/serviceapp/addNotesDriver.php index 6f0b8424..11816dbb 100644 --- a/backend/serviceapp/addNotesDriver.php +++ b/backend/serviceapp/addNotesDriver.php @@ -9,10 +9,12 @@ $editor = filterRequest("editor"); // Encrypt the phone number $encryptedPhone = $encryptionHelper->encryptData($phone); +// مفتاح الربط بالحساب — نفس صيغة otpPhoneKey المستعملة في phone_key +$phoneKey = otpPhoneKey($phone); // SQL query: insert new row OR update existing one if phone already exists -$sql = "INSERT INTO `notesForDriverService` (`phone`, `note`, `editor`) - VALUES (:phone, :note, :editor) +$sql = "INSERT INTO `notesForDriverService` (`phone`, `phone_key`, `note`, `editor`) + VALUES (:phone, :phone_key, :note, :editor) ON DUPLICATE KEY UPDATE `note` = VALUES(`note`), `editor` = VALUES(`editor`)"; @@ -22,6 +24,7 @@ $stmt = $con->prepare($sql); // Bind the parameters $stmt->bindParam(':phone', $encryptedPhone); +$stmt->bindParam(':phone_key', $phoneKey); $stmt->bindParam(':note', $note); $stmt->bindParam(':editor', $editor); diff --git a/backend/serviceapp/driverWhoregisterFfterCall.php b/backend/serviceapp/driverWhoregisterFfterCall.php index 84531508..7623e036 100644 --- a/backend/serviceapp/driverWhoregisterFfterCall.php +++ b/backend/serviceapp/driverWhoregisterFfterCall.php @@ -14,7 +14,7 @@ SELECT n.created_at AS note_created_at FROM driver d -LEFT JOIN notesForDriverService n ON n.phone = d.phone +LEFT JOIN notesForDriverService n ON n.phone_key = d.phone_key WHERE MONTH(d.created_at) = MONTH(CURRENT_DATE()) AND n.phone IS NOT NULL diff --git a/backend/serviceapp/getJsonFile.php b/backend/serviceapp/getJsonFile.php index d15e2ea3..28882eb5 100644 --- a/backend/serviceapp/getJsonFile.php +++ b/backend/serviceapp/getJsonFile.php @@ -24,7 +24,7 @@ FROM LEFT JOIN driver d ON pv.phone_number = d.phone_key LEFT JOIN - notesForDriverService n ON pv.phone_number = n.phone + notesForDriverService n ON pv.phone_number = n.phone_key WHERE d.phone IS NULL AND (n.note != 'delete' OR n.note IS NULL) diff --git a/backend/serviceapp/getdriverstotalMonthly.php b/backend/serviceapp/getdriverstotalMonthly.php index 04739b65..d5ebf32a 100644 --- a/backend/serviceapp/getdriverstotalMonthly.php +++ b/backend/serviceapp/getdriverstotalMonthly.php @@ -48,7 +48,7 @@ SELECT ( SELECT COUNT(*) FROM notesForDriverService n - JOIN driver d ON n.phone = d.phone + JOIN driver d ON n.phone_key = d.phone_key WHERE DATE(n.createdAt) = date_series.date ) AS dailyMatchingNotes, @@ -67,7 +67,7 @@ SELECT ( SELECT COUNT(*) FROM notesForDriverService n - JOIN driver d ON n.phone = d.phone + JOIN driver d ON n.phone_key = d.phone_key WHERE n.createdAt BETWEEN :start_date3 AND :end_date3 ) AS totalMonthlyMatchingNotes diff --git a/backend/serviceapp/register.php b/backend/serviceapp/register.php index fe45bd22..8d7b60a6 100644 --- a/backend/serviceapp/register.php +++ b/backend/serviceapp/register.php @@ -35,14 +35,26 @@ try { // 1. التحقق من عدم وجود الحساب مسبقاً (عن طريق البريد الإلكتروني، الهاتف أو البصمة) $fpHash = hash('sha256', $fingerprint); - $check = $con->prepare("SELECT id FROM users WHERE email = ? OR phone = ? OR fingerprint_hash = ? LIMIT 1"); + global $blindIndex; + $emailBidx = $blindIndex ? $blindIndex->index('users.email', $email) : null; + $phoneBidx = $blindIndex ? $blindIndex->index('users.phone', $phone) : null; + + // كشف التكرار عبر الفهرس أيضاً: بدونه يُقبل نفس البريد مرتين تحت التشفير + // العشوائي لأن النصين المشفّرين لن يتطابقا. + $check = $con->prepare( + "SELECT id FROM users + WHERE email = ? OR phone = ? OR fingerprint_hash = ? + OR (? IS NOT NULL AND email_bidx = ?) + OR (? IS NOT NULL AND phone_bidx = ?) + LIMIT 1" + ); // تشفير الحقول للبحث عنها إذا كانت مشفرة في قاعدة البيانات (حسب تصميم النظام) $encEmail = $encryptionHelper->encryptData($email); // ملاحظة: البحث بالهاتف والبريد المشفر يتطلب مطابقة دقيقة أو البحث بالـ Hash إذا كان متوفراً // هنا سنفترض البحث بالبيانات الممرة مباشرة أو المشفرة حسب ما تقتضيه سياسة connect.php - $check->execute([$email, $phone, $fpHash]); + $check->execute([$email, $phone, $fpHash, $emailBidx, $emailBidx, $phoneBidx, $phoneBidx]); if ($check->rowCount() > 0) { jsonError("هذا الحساب أو الجهاز مسجل مسبقاً."); @@ -61,8 +73,8 @@ try { $encFp = $encryptionHelper->encryptData($fingerprint); // 3. الإدخال في قاعدة البيانات (الحالة الافتراضية هي 0 أو pending) - $sql = "INSERT INTO users (id, first_name, last_name, email, phone, password, fingerprint, fingerprint_hash, user_type, created_at) - VALUES (:id, :fname, :lname, :email, :phone, :pass, :fp, :fp_hash, 'service', NOW())"; + $sql = "INSERT INTO users (id, first_name, last_name, email, phone, password, fingerprint, fingerprint_hash, user_type, created_at, email_bidx, phone_bidx) + VALUES (:id, :fname, :lname, :email, :phone, :pass, :fp, :fp_hash, 'service', NOW(), :email_bidx, :phone_bidx)"; $stmt = $con->prepare($sql); @@ -74,7 +86,9 @@ try { ':phone' => $encPhone, ':pass' => $hashedPassword, ':fp' => $encFp, - ':fp_hash' => $fpHash + ':fp_hash' => $fpHash, + ':email_bidx' => $emailBidx, + ':phone_bidx' => $phoneBidx ]); printSuccess([