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:
co-authored by
Claude Opus 5
parent
35a66935aa
commit
2135edcf43
@@ -25,13 +25,34 @@ $sql = "SELECT
|
|||||||
driver.maritalStatus,
|
driver.maritalStatus,
|
||||||
driver.created_at,
|
driver.created_at,
|
||||||
driver.updated_at,
|
driver.updated_at,
|
||||||
email_verifications.verified
|
driver.email AS _email_enc
|
||||||
FROM
|
FROM
|
||||||
driver
|
driver
|
||||||
LEFT JOIN email_verifications ON email_verifications.email = driver.email
|
|
||||||
WHERE
|
WHERE
|
||||||
driver.phone = :phone AND driver.email = :email";
|
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 = $con->prepare($sql);
|
||||||
$stmt->bindParam(':email', $email);
|
$stmt->bindParam(':email', $email);
|
||||||
$stmt->bindParam(':phone', $phone);
|
$stmt->bindParam(':phone', $phone);
|
||||||
@@ -39,6 +60,12 @@ $stmt->execute();
|
|||||||
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
$count = $stmt->rowCount();
|
$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) {
|
if ($count > 0) {
|
||||||
$stored_password = $data[0]['password'];
|
$stored_password = $data[0]['password'];
|
||||||
if (password_verify($password, $stored_password)) {
|
if (password_verify($password, $stored_password)) {
|
||||||
|
|||||||
+27
-2
@@ -62,18 +62,43 @@ $sql = "SELECT
|
|||||||
passengers.`maritalStatus`,
|
passengers.`maritalStatus`,
|
||||||
passengers.`created_at`,
|
passengers.`created_at`,
|
||||||
passengers.`updated_at`,
|
passengers.`updated_at`,
|
||||||
email_verifications.verified
|
passengers.`email` AS `_email_enc`
|
||||||
FROM
|
FROM
|
||||||
`passengers`
|
`passengers`
|
||||||
LEFT JOIN email_verifications ON email_verifications.email = passengers.email
|
|
||||||
WHERE
|
WHERE
|
||||||
$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 = $con->prepare($sql);
|
||||||
$stmt->execute($params);
|
$stmt->execute($params);
|
||||||
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
$count = $stmt->rowCount();
|
$count = $stmt->rowCount();
|
||||||
|
|
||||||
if ($count > 0) {
|
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'];
|
$stored_password = $data[0]['password'];
|
||||||
if (password_verify($password, $stored_password)) {
|
if (password_verify($password, $stored_password)) {
|
||||||
unset($data[0]['password']);
|
unset($data[0]['password']);
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ require_once __DIR__ . '/../connect.php';
|
|||||||
$email = filterRequest("email");
|
$email = filterRequest("email");
|
||||||
$token = filterRequest("token");
|
$token = filterRequest("token");
|
||||||
|
|
||||||
$sql = "SELECT * FROM `email_verifications` WHERE `email` = '$email'";
|
$stmt = $con->prepare("SELECT * FROM `email_verifications` WHERE `email` = ?");
|
||||||
$stmt = $con->prepare($sql);
|
$stmt->execute([$email]);
|
||||||
$stmt->execute();
|
|
||||||
|
|
||||||
$rowCount = $stmt->rowCount();
|
$rowCount = $stmt->rowCount();
|
||||||
|
|
||||||
@@ -41,9 +40,9 @@ SEFER Team.
|
|||||||
|
|
||||||
if ($rowCount > 0) {
|
if ($rowCount > 0) {
|
||||||
// The email already exists, so update the data
|
// The email already exists, so update the data
|
||||||
$sql = "UPDATE `email_verifications` SET `token` = '$token' WHERE `email` = '$email'";
|
// كانت القيم تُدمج في نص الاستعلام مباشرةً — حقن SQL عبر البريد أو الرمز.
|
||||||
$stmt = $con->prepare($sql);
|
$stmt = $con->prepare("UPDATE `email_verifications` SET `token` = ? WHERE `email` = ?");
|
||||||
$stmt->execute();
|
$stmt->execute([$token, $email]);
|
||||||
|
|
||||||
if ($stmt->rowCount() > 0) {
|
if ($stmt->rowCount() > 0) {
|
||||||
// The update was successful
|
// The update was successful
|
||||||
@@ -55,9 +54,8 @@ if ($rowCount > 0) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// The email does not exist, so insert the data
|
// The email does not exist, so insert the data
|
||||||
$sql = "INSERT INTO `email_verifications` (`email`, `token`) VALUES ('$email', '$token')";
|
$stmt = $con->prepare("INSERT INTO `email_verifications` (`email`, `token`) VALUES (?, ?)");
|
||||||
$stmt = $con->prepare($sql);
|
$stmt->execute([$email, $token]);
|
||||||
$stmt->execute();
|
|
||||||
|
|
||||||
if ($stmt->rowCount() > 0) {
|
if ($stmt->rowCount() > 0) {
|
||||||
// The insertion was successful
|
// The insertion was successful
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ $columns = [
|
|||||||
['driver', 'phone_key', "VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط جداول التحقق'"],
|
['driver', 'phone_key', "VARCHAR(80) NULL DEFAULT NULL COMMENT 'مفتاح ربط جداول التحقق'"],
|
||||||
['passengers', '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', 'email_bidx', "CHAR(64) NULL DEFAULT NULL COMMENT 'HMAC لبريد موظف الخدمة'"],
|
||||||
['users', 'phone_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_national_bidx', 'national_bidx'],
|
||||||
['driver', 'idx_driver_phone_key', 'phone_key'],
|
['driver', 'idx_driver_phone_key', 'phone_key'],
|
||||||
['passengers', 'idx_passengers_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;
|
$applied = 0;
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ $editor = filterRequest("editor");
|
|||||||
|
|
||||||
// Encrypt the phone number
|
// Encrypt the phone number
|
||||||
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
||||||
|
// مفتاح الربط بالحساب — نفس صيغة otpPhoneKey المستعملة في phone_key
|
||||||
|
$phoneKey = otpPhoneKey($phone);
|
||||||
|
|
||||||
// SQL query: insert new row OR update existing one if phone already exists
|
// SQL query: insert new row OR update existing one if phone already exists
|
||||||
$sql = "INSERT INTO `notesForDriverService` (`phone`, `note`, `editor`)
|
$sql = "INSERT INTO `notesForDriverService` (`phone`, `phone_key`, `note`, `editor`)
|
||||||
VALUES (:phone, :note, :editor)
|
VALUES (:phone, :phone_key, :note, :editor)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
`note` = VALUES(`note`),
|
`note` = VALUES(`note`),
|
||||||
`editor` = VALUES(`editor`)";
|
`editor` = VALUES(`editor`)";
|
||||||
@@ -22,6 +24,7 @@ $stmt = $con->prepare($sql);
|
|||||||
|
|
||||||
// Bind the parameters
|
// Bind the parameters
|
||||||
$stmt->bindParam(':phone', $encryptedPhone);
|
$stmt->bindParam(':phone', $encryptedPhone);
|
||||||
|
$stmt->bindParam(':phone_key', $phoneKey);
|
||||||
$stmt->bindParam(':note', $note);
|
$stmt->bindParam(':note', $note);
|
||||||
$stmt->bindParam(':editor', $editor);
|
$stmt->bindParam(':editor', $editor);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ SELECT
|
|||||||
n.created_at AS note_created_at
|
n.created_at AS note_created_at
|
||||||
FROM
|
FROM
|
||||||
driver d
|
driver d
|
||||||
LEFT JOIN notesForDriverService n ON n.phone = d.phone
|
LEFT JOIN notesForDriverService n ON n.phone_key = d.phone_key
|
||||||
WHERE
|
WHERE
|
||||||
MONTH(d.created_at) = MONTH(CURRENT_DATE())
|
MONTH(d.created_at) = MONTH(CURRENT_DATE())
|
||||||
AND n.phone IS NOT NULL
|
AND n.phone IS NOT NULL
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ FROM
|
|||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
driver d ON pv.phone_number = d.phone_key
|
driver d ON pv.phone_number = d.phone_key
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
notesForDriverService n ON pv.phone_number = n.phone
|
notesForDriverService n ON pv.phone_number = n.phone_key
|
||||||
WHERE
|
WHERE
|
||||||
d.phone IS NULL
|
d.phone IS NULL
|
||||||
AND (n.note != 'delete' OR n.note IS NULL)
|
AND (n.note != 'delete' OR n.note IS NULL)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ SELECT
|
|||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM notesForDriverService n
|
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
|
WHERE DATE(n.createdAt) = date_series.date
|
||||||
) AS dailyMatchingNotes,
|
) AS dailyMatchingNotes,
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ SELECT
|
|||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM notesForDriverService n
|
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
|
WHERE n.createdAt BETWEEN :start_date3 AND :end_date3
|
||||||
) AS totalMonthlyMatchingNotes
|
) AS totalMonthlyMatchingNotes
|
||||||
|
|
||||||
|
|||||||
@@ -35,14 +35,26 @@ try {
|
|||||||
|
|
||||||
// 1. التحقق من عدم وجود الحساب مسبقاً (عن طريق البريد الإلكتروني، الهاتف أو البصمة)
|
// 1. التحقق من عدم وجود الحساب مسبقاً (عن طريق البريد الإلكتروني، الهاتف أو البصمة)
|
||||||
$fpHash = hash('sha256', $fingerprint);
|
$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);
|
$encEmail = $encryptionHelper->encryptData($email);
|
||||||
// ملاحظة: البحث بالهاتف والبريد المشفر يتطلب مطابقة دقيقة أو البحث بالـ Hash إذا كان متوفراً
|
// ملاحظة: البحث بالهاتف والبريد المشفر يتطلب مطابقة دقيقة أو البحث بالـ Hash إذا كان متوفراً
|
||||||
// هنا سنفترض البحث بالبيانات الممرة مباشرة أو المشفرة حسب ما تقتضيه سياسة connect.php
|
// هنا سنفترض البحث بالبيانات الممرة مباشرة أو المشفرة حسب ما تقتضيه سياسة connect.php
|
||||||
|
|
||||||
$check->execute([$email, $phone, $fpHash]);
|
$check->execute([$email, $phone, $fpHash, $emailBidx, $emailBidx, $phoneBidx, $phoneBidx]);
|
||||||
|
|
||||||
if ($check->rowCount() > 0) {
|
if ($check->rowCount() > 0) {
|
||||||
jsonError("هذا الحساب أو الجهاز مسجل مسبقاً.");
|
jsonError("هذا الحساب أو الجهاز مسجل مسبقاً.");
|
||||||
@@ -61,8 +73,8 @@ try {
|
|||||||
$encFp = $encryptionHelper->encryptData($fingerprint);
|
$encFp = $encryptionHelper->encryptData($fingerprint);
|
||||||
|
|
||||||
// 3. الإدخال في قاعدة البيانات (الحالة الافتراضية هي 0 أو pending)
|
// 3. الإدخال في قاعدة البيانات (الحالة الافتراضية هي 0 أو pending)
|
||||||
$sql = "INSERT INTO users (id, first_name, last_name, email, phone, password, fingerprint, fingerprint_hash, user_type, created_at)
|
$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())";
|
VALUES (:id, :fname, :lname, :email, :phone, :pass, :fp, :fp_hash, 'service', NOW(), :email_bidx, :phone_bidx)";
|
||||||
|
|
||||||
|
|
||||||
$stmt = $con->prepare($sql);
|
$stmt = $con->prepare($sql);
|
||||||
@@ -74,7 +86,9 @@ try {
|
|||||||
':phone' => $encPhone,
|
':phone' => $encPhone,
|
||||||
':pass' => $hashedPassword,
|
':pass' => $hashedPassword,
|
||||||
':fp' => $encFp,
|
':fp' => $encFp,
|
||||||
':fp_hash' => $fpHash
|
':fp_hash' => $fpHash,
|
||||||
|
':email_bidx' => $emailBidx,
|
||||||
|
':phone_bidx' => $phoneBidx
|
||||||
]);
|
]);
|
||||||
|
|
||||||
printSuccess([
|
printSuccess([
|
||||||
|
|||||||
Reference in New Issue
Block a user