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 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-25 16:48:10 +03:00
co-authored by Claude Opus 5
parent 35a66935aa
commit 2135edcf43
9 changed files with 98 additions and 24 deletions
+29 -2
View File
@@ -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)) {
+27 -2
View File
@@ -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']);
+7 -9
View File
@@ -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
+7
View File
@@ -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;
+5 -2
View File
@@ -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);
@@ -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
+1 -1
View File
@@ -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)
@@ -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
+19 -5
View File
@@ -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([