From 0c0d980b024149539136ffaaa4f011c2db3aa74d Mon Sep 17 00:00:00 2001 From: Pinga <121483313+getpinga@users.noreply.github.com> Date: Tue, 5 Mar 2024 15:25:02 +0200 Subject: [PATCH] Fixed potential domain transfer security issue --- automation/auto-approve-transfer.php | 116 ++++++++++++++++++++++- automation/helpers.php | 30 ++++++ cp/app/Controllers/DomainsController.php | 102 +++++++++++++++++++- cp/bootstrap/helper.php | 30 ++++++ epp/src/epp-transfer.php | 110 ++++++++++++++++++++- epp/src/helpers.php | 30 ++++++ 6 files changed, 413 insertions(+), 5 deletions(-) diff --git a/automation/auto-approve-transfer.php b/automation/auto-approve-transfer.php index 5eb051e..54280d8 100644 --- a/automation/auto-approve-transfer.php +++ b/automation/auto-approve-transfer.php @@ -17,6 +17,8 @@ try { } try { + $dbh->beginTransaction(); + $query_domain = "SELECT id, name, registrant, crdate, exdate, lastupdate, clid, crid, upid, trdate, trstatus, reid, redate, acid, acdate, transfer_exdate FROM domain WHERE CURRENT_TIMESTAMP > acdate AND trstatus = 'pending'"; $stmt_domain = $dbh->prepare($query_domain); $stmt_domain->execute(); @@ -56,12 +58,118 @@ try { continue; } } + + // Fetch contact map + $stmt = $dbh->prepare('SELECT contact_id, type FROM domain_contact_map WHERE domain_id = ?'); + $stmt->execute([$domain_id]); + $contactMap = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Prepare an array to hold new contact IDs to prevent duplicating contacts + $newContactIds = []; + + // Copy registrant data + $stmt = $dbh->prepare('SELECT * FROM contact WHERE id = ?'); + $stmt->execute([$registrant]); + $registrantData = $stmt->fetch(PDO::FETCH_ASSOC); + unset($registrantData['id']); + $registrantData['identifier'] = generateAuthInfo(); + $registrantData['clid'] = $reid; + + $stmt = $dbh->prepare('INSERT INTO contact (' . implode(', ', array_keys($registrantData)) . ') VALUES (:' . implode(', :', array_keys($registrantData)) . ')'); + foreach ($registrantData as $key => $value) { + $stmt->bindValue(':' . $key, $value); + } + $stmt->execute(); + $newRegistrantId = $dbh->lastInsertId(); + $newContactIds[$registrant] = $newRegistrantId; + + // Copy postal info for the registrant + $stmt = $dbh->prepare('SELECT * FROM contact_postalInfo WHERE contact_id = ?'); + $stmt->execute([$registrant]); + $postalInfos = $stmt->fetchAll(PDO::FETCH_ASSOC); + + foreach ($postalInfos as $postalInfo) { + unset($postalInfo['id']); + $postalInfo['contact_id'] = $newRegistrantId; + $columns = array_keys($postalInfo); + $stmt = $dbh->prepare('INSERT INTO contact_postalInfo (' . implode(', ', $columns) . ') VALUES (:' . implode(', :', $columns) . ')'); + foreach ($postalInfo as $key => $value) { + $stmt->bindValue(':' . $key, $value); + } + $stmt->execute(); + } + + // Insert auth info and status for the new registrant + $new_authinfo = generateAuthInfo(); + $dbh->prepare('INSERT INTO contact_authInfo (contact_id, authtype, authinfo) VALUES (?, ?, ?)')->execute([$newRegistrantId, 'pw', $new_authinfo]); + $dbh->prepare('INSERT INTO contact_status (contact_id, status) VALUES (?, ?)')->execute([$newRegistrantId, 'ok']); + + // Process each contact in the contact map + foreach ($contactMap as $contact) { + if (!array_key_exists($contact['contact_id'], $newContactIds)) { + $stmt = $dbh->prepare('SELECT * FROM contact WHERE id = ?'); + $stmt->execute([$contact['contact_id']]); + $contactData = $stmt->fetch(PDO::FETCH_ASSOC); + unset($contactData['id']); + $contactData['identifier'] = generateAuthInfo(); + $contactData['clid'] = $reid; + + $stmt = $dbh->prepare('INSERT INTO contact (' . implode(', ', array_keys($contactData)) . ') VALUES (:' . implode(', :', array_keys($contactData)) . ')'); + foreach ($contactData as $key => $value) { + $stmt->bindValue(':' . $key, $value); + } + $stmt->execute(); + $newContactId = $dbh->lastInsertId(); + $newContactIds[$contact['contact_id']] = $newContactId; + + // Repeat postal info and auth info/status insertion for each new contact + $stmt = $dbh->prepare('SELECT * FROM contact_postalInfo WHERE contact_id = ?'); + $stmt->execute([$contact['contact_id']]); + $postalInfos = $stmt->fetchAll(PDO::FETCH_ASSOC); + + foreach ($postalInfos as $postalInfo) { + unset($postalInfo['id']); + $postalInfo['contact_id'] = $newContactId; + $columns = array_keys($postalInfo); + $stmt = $dbh->prepare('INSERT INTO contact_postalInfo (' . implode(', ', $columns) . ') VALUES (:' . implode(', :', $columns) . ')'); + foreach ($postalInfo as $key => $value) { + $stmt->bindValue(':' . $key, $value); + } + $stmt->execute(); + } + + $new_authinfo = generateAuthInfo(); + $dbh->prepare('INSERT INTO contact_authInfo (contact_id, authtype, authinfo) VALUES (?, ?, ?)')->execute([$newContactId, 'pw', $new_authinfo]); + $dbh->prepare('INSERT INTO contact_status (contact_id, status) VALUES (?, ?)')->execute([$newContactId, 'ok']); + } + } $from = $dbh->query("SELECT exdate FROM domain WHERE id = '$domain_id' LIMIT 1")->fetchColumn(); - $stmt_update = $dbh->prepare("UPDATE domain SET exdate = DATE_ADD(exdate, INTERVAL $date_add MONTH), lastupdate = CURRENT_TIMESTAMP, clid = '$reid', upid = '$clid', trdate = CURRENT_TIMESTAMP, trstatus = 'serverApproved', acdate = CURRENT_TIMESTAMP, transfer_exdate = NULL WHERE id = '$domain_id'"); + $stmt_update = $dbh->prepare("UPDATE domain SET exdate = DATE_ADD(exdate, INTERVAL $date_add MONTH), lastupdate = CURRENT_TIMESTAMP, clid = '$reid', upid = '$clid', registrant = '$newRegistrantId', trdate = CURRENT_TIMESTAMP, trstatus = 'serverApproved', acdate = CURRENT_TIMESTAMP, transfer_exdate = NULL WHERE id = '$domain_id'"); $stmt_update->execute(); + $new_authinfo = generateAuthInfo(); + $stmt_update_auth = $dbh->prepare("UPDATE domain_authInfo SET authinfo = '$new_authinfo' WHERE domain_id = '$domain_id'"); + $stmt_update_auth->execute(); + + foreach ($contactMap as $contact) { + // Construct the SQL update query + $sql = "UPDATE domain_contact_map SET contact_id = :new_contact_id WHERE domain_id = :domain_id AND type = :type AND contact_id = :contact_id"; + + // Prepare the SQL statement + $stmt = $dbh->prepare($sql); + + // Bind the values to the placeholders + $stmt->bindValue(':new_contact_id', $newContactIds[$contact['contact_id']]); + $stmt->bindValue(':domain_id', $domain_id); + $stmt->bindValue(':type', $contact['type']); + $stmt->bindValue(':contact_id', $contact['contact_id']); + + // Execute the update statement + $stmt->execute(); + } + $stmt_update_host = $dbh->prepare("UPDATE host SET clid = '$reid', upid = NULL, lastupdate = CURRENT_TIMESTAMP, trdate = CURRENT_TIMESTAMP WHERE domain_id = '$domain_id'"); $stmt_update_host->execute(); @@ -112,9 +220,15 @@ try { } } $stmt_contact = null; + $dbh->commit(); $log->info('job finished successfully.'); } catch (PDOException $e) { + $dbh->rollBack(); + $log->error('Database error: ' . $e->getMessage()); +} catch (PDOException $e) { + $dbh->rollBack(); $log->error('Database error: ' . $e->getMessage()); } catch (Throwable $e) { + $dbh->rollBack(); $log->error('Error: ' . $e->getMessage()); } \ No newline at end of file diff --git a/automation/helpers.php b/automation/helpers.php index e3d8ffd..f19b570 100644 --- a/automation/helpers.php +++ b/automation/helpers.php @@ -165,4 +165,34 @@ function getDomainPrice($pdo, $domain_name, $tld_id, $date_add = 12, $command = } return ['type' => 'not_found', 'price' => 0]; +} + +function generateAuthInfo(): string { + $length = 16; + $charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + $retVal = ""; + $digitCount = 0; + + // Generate initial random string + for ($i = 0; $i < $length; $i++) { + $randomIndex = random_int(0, strlen($charset) - 1); + $char = $charset[$randomIndex]; + $retVal .= $char; + if ($char >= '0' && $char <= '9') { + $digitCount++; + } + } + + // Ensure there are at least two digits in the string + while ($digitCount < 2) { + // Replace a non-digit character at a random position with a digit + $replacePosition = random_int(0, $length - 1); + if (!($retVal[$replacePosition] >= '0' && $retVal[$replacePosition] <= '9')) { + $randomDigit = random_int(0, 9); // Generate a digit from 0 to 9 + $retVal = substr_replace($retVal, (string)$randomDigit, $replacePosition, 1); + $digitCount++; + } + } + + return $retVal; } \ No newline at end of file diff --git a/cp/app/Controllers/DomainsController.php b/cp/app/Controllers/DomainsController.php index 3741018..b82a648 100644 --- a/cp/app/Controllers/DomainsController.php +++ b/cp/app/Controllers/DomainsController.php @@ -2703,6 +2703,88 @@ class DomainsController extends Controller try { $db->beginTransaction(); + $contactMap = $db->select('SELECT contact_id, type FROM domain_contact_map WHERE domain_id = ?', [$domain_id]); + + // Prepare an array to hold new contact IDs to prevent duplicating contacts + $newContactIds = []; + + $registrantData = $db->selectRow('SELECT * FROM contact WHERE id = ?', [$registrant]); + unset($registrantData['id']); // Remove the ID to ensure a new record is created + $registrantData['identifier'] = generateAuthInfo(); + $registrantData['clid'] = $reid; + $db->insert('contact', $registrantData); + $newRegistrantId = $db->getlastInsertId(); + $newContactIds[$registrant] = $newRegistrantId; + + // Fetch associated contact_postalInfo records + $postalInfos = $db->select('SELECT * FROM contact_postalInfo WHERE contact_id = ?', [$registrant]); + + foreach ($postalInfos as $postalInfo) { + unset($postalInfo['id']); // Remove the ID to ensure a new record is created + $postalInfo['contact_id'] = $newRegistrantId; // Replace with new contact ID + + // Insert new contact_postalInfo record + $db->insert('contact_postalInfo', $postalInfo); + } + + $new_authinfo = generateAuthInfo(); + $db->insert( + 'contact_authInfo', + [ + 'contact_id' => $newRegistrantId, + 'authtype' => 'pw', + 'authinfo' => $new_authinfo + ] + ); + + $db->insert( + 'contact_status', + [ + 'contact_id' => $newRegistrantId, + 'status' => 'ok' + ] + ); + + foreach ($contactMap as $contact) { + if (!array_key_exists($contact['contact_id'], $newContactIds)) { // Check if not already copied + $contactData = $db->selectRow('SELECT * FROM contact WHERE id = ?', [$contact['contact_id']]); + unset($contactData['id']); // Remove the ID to ensure a new record is created + $contactData['identifier'] = generateAuthInfo(); + $contactData['clid'] = $reid; + $db->insert('contact', $contactData); + $newContactId = $db->getlastInsertId(); + $newContactIds[$contact['contact_id']] = $newContactId; + + // Fetch and copy associated contact_postalInfo records + $postalInfos = $db->select('SELECT * FROM contact_postalInfo WHERE contact_id = ?', [$contact['contact_id']]); + foreach ($postalInfos as $postalInfo) { + unset($postalInfo['id']); // Ensure a new record is created + $postalInfo['contact_id'] = $newContactId; // Assign to new contact ID + + // Insert new contact_postalInfo record + $db->insert('contact_postalInfo', $postalInfo); + } + + $new_authinfo = generateAuthInfo(); + $db->insert( + 'contact_authInfo', + [ + 'contact_id' => $newContactId, + 'authtype' => 'pw', + 'authinfo' => $new_authinfo + ] + ); + + $db->insert( + 'contact_status', + [ + 'contact_id' => $newContactId, + 'status' => 'ok' + ] + ); + } + } + $row = $db->selectRow( 'SELECT exdate FROM domain WHERE name = ? LIMIT 1', [$domainName] @@ -2710,9 +2792,25 @@ class DomainsController extends Controller $from = $row['exdate']; $db->exec( - 'UPDATE domain SET exdate = DATE_ADD(exdate, INTERVAL ? MONTH), lastupdate = CURRENT_TIMESTAMP(3), clid = ?, upid = ?, trdate = CURRENT_TIMESTAMP(3), trstatus = ?, acdate = CURRENT_TIMESTAMP(3), transfer_exdate = NULL, rgpstatus = ?, transferPeriod = ? WHERE id = ?', - [$date_add, $reid, $clid, 'clientApproved', 'transferPeriod', $date_add, $domain_id] + 'UPDATE domain SET exdate = DATE_ADD(exdate, INTERVAL ? MONTH), lastupdate = CURRENT_TIMESTAMP(3), clid = ?, upid = ?, registrant = ?, trdate = CURRENT_TIMESTAMP(3), trstatus = ?, acdate = CURRENT_TIMESTAMP(3), transfer_exdate = NULL, rgpstatus = ?, transferPeriod = ? WHERE id = ?', + [$date_add, $reid, $clid, $newRegistrantId, 'clientApproved', 'transferPeriod', $date_add, $domain_id] ); + + $new_authinfo = generateAuthInfo(); + $db->exec( + 'UPDATE domain_authInfo SET authinfo = ? WHERE domain_id = ?', + [$new_authinfo, $domain_id] + ); + + foreach ($contactMap as $contact) { + $db->update('domain_contact_map', [ + 'contact_id' => $newContactIds[$contact['contact_id']], + ], [ + 'domain_id' => $domain_id, + 'type' => $contact['type'], + 'contact_id' => $contact['contact_id'] // Ensure we're updating the correct existing record + ]); + } $db->exec( 'UPDATE host SET clid = ?, upid = ?, lastupdate = CURRENT_TIMESTAMP(3), trdate = CURRENT_TIMESTAMP(3) WHERE domain_id = ?', diff --git a/cp/bootstrap/helper.php b/cp/bootstrap/helper.php index ec376d0..2b518fd 100644 --- a/cp/bootstrap/helper.php +++ b/cp/bootstrap/helper.php @@ -478,4 +478,34 @@ function normalizePhoneNumber($number, $defaultRegion = 'US') { } catch (NumberParseException $e) { return ['error' => 'Failed to parse and normalize phone number: ' . $e->getMessage()]; } +} + +function generateAuthInfo(): string { + $length = 16; + $charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + $retVal = ""; + $digitCount = 0; + + // Generate initial random string + for ($i = 0; $i < $length; $i++) { + $randomIndex = random_int(0, strlen($charset) - 1); + $char = $charset[$randomIndex]; + $retVal .= $char; + if ($char >= '0' && $char <= '9') { + $digitCount++; + } + } + + // Ensure there are at least two digits in the string + while ($digitCount < 2) { + // Replace a non-digit character at a random position with a digit + $replacePosition = random_int(0, $length - 1); + if (!($retVal[$replacePosition] >= '0' && $retVal[$replacePosition] <= '9')) { + $randomDigit = random_int(0, 9); // Generate a digit from 0 to 9 + $retVal = substr_replace($retVal, (string)$randomDigit, $replacePosition, 1); + $digitCount++; + } + } + + return $retVal; } \ No newline at end of file diff --git a/epp/src/epp-transfer.php b/epp/src/epp-transfer.php index 7589b5a..5656917 100644 --- a/epp/src/epp-transfer.php +++ b/epp/src/epp-transfer.php @@ -478,12 +478,118 @@ function processDomainTransfer($conn, $db, $xml, $clid, $database_type, $trans) } } + // Fetch contact map + $stmt = $db->prepare('SELECT contact_id, type FROM domain_contact_map WHERE domain_id = ?'); + $stmt->execute([$domain_id]); + $contactMap = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Prepare an array to hold new contact IDs to prevent duplicating contacts + $newContactIds = []; + + // Copy registrant data + $stmt = $db->prepare('SELECT * FROM contact WHERE id = ?'); + $stmt->execute([$registrant]); + $registrantData = $stmt->fetch(PDO::FETCH_ASSOC); + unset($registrantData['id']); + $registrantData['identifier'] = generateAuthInfo(); + $registrantData['clid'] = $reid; + + $stmt = $db->prepare('INSERT INTO contact (' . implode(', ', array_keys($registrantData)) . ') VALUES (:' . implode(', :', array_keys($registrantData)) . ')'); + foreach ($registrantData as $key => $value) { + $stmt->bindValue(':' . $key, $value); + } + $stmt->execute(); + $newRegistrantId = $db->lastInsertId(); + $newContactIds[$registrant] = $newRegistrantId; + + // Copy postal info for the registrant + $stmt = $db->prepare('SELECT * FROM contact_postalInfo WHERE contact_id = ?'); + $stmt->execute([$registrant]); + $postalInfos = $stmt->fetchAll(PDO::FETCH_ASSOC); + + foreach ($postalInfos as $postalInfo) { + unset($postalInfo['id']); + $postalInfo['contact_id'] = $newRegistrantId; + $columns = array_keys($postalInfo); + $stmt = $db->prepare('INSERT INTO contact_postalInfo (' . implode(', ', $columns) . ') VALUES (:' . implode(', :', $columns) . ')'); + foreach ($postalInfo as $key => $value) { + $stmt->bindValue(':' . $key, $value); + } + $stmt->execute(); + } + + // Insert auth info and status for the new registrant + $new_authinfo = generateAuthInfo(); + $db->prepare('INSERT INTO contact_authInfo (contact_id, authtype, authinfo) VALUES (?, ?, ?)')->execute([$newRegistrantId, 'pw', $new_authinfo]); + $db->prepare('INSERT INTO contact_status (contact_id, status) VALUES (?, ?)')->execute([$newRegistrantId, 'ok']); + + // Process each contact in the contact map + foreach ($contactMap as $contact) { + if (!array_key_exists($contact['contact_id'], $newContactIds)) { + $stmt = $db->prepare('SELECT * FROM contact WHERE id = ?'); + $stmt->execute([$contact['contact_id']]); + $contactData = $stmt->fetch(PDO::FETCH_ASSOC); + unset($contactData['id']); + $contactData['identifier'] = generateAuthInfo(); + $contactData['clid'] = $reid; + + $stmt = $db->prepare('INSERT INTO contact (' . implode(', ', array_keys($contactData)) . ') VALUES (:' . implode(', :', array_keys($contactData)) . ')'); + foreach ($contactData as $key => $value) { + $stmt->bindValue(':' . $key, $value); + } + $stmt->execute(); + $newContactId = $db->lastInsertId(); + $newContactIds[$contact['contact_id']] = $newContactId; + + // Repeat postal info and auth info/status insertion for each new contact + $stmt = $db->prepare('SELECT * FROM contact_postalInfo WHERE contact_id = ?'); + $stmt->execute([$contact['contact_id']]); + $postalInfos = $stmt->fetchAll(PDO::FETCH_ASSOC); + + foreach ($postalInfos as $postalInfo) { + unset($postalInfo['id']); + $postalInfo['contact_id'] = $newContactId; + $columns = array_keys($postalInfo); + $stmt = $db->prepare('INSERT INTO contact_postalInfo (' . implode(', ', $columns) . ') VALUES (:' . implode(', :', $columns) . ')'); + foreach ($postalInfo as $key => $value) { + $stmt->bindValue(':' . $key, $value); + } + $stmt->execute(); + } + + $new_authinfo = generateAuthInfo(); + $db->prepare('INSERT INTO contact_authInfo (contact_id, authtype, authinfo) VALUES (?, ?, ?)')->execute([$newContactId, 'pw', $new_authinfo]); + $db->prepare('INSERT INTO contact_status (contact_id, status) VALUES (?, ?)')->execute([$newContactId, 'ok']); + } + } + $stmt = $db->prepare("SELECT exdate FROM domain WHERE id = :domain_id LIMIT 1"); $stmt->execute(['domain_id' => $domain_id]); $from = $stmt->fetchColumn(); - $stmt = $db->prepare("UPDATE domain SET exdate = DATE_ADD(exdate, INTERVAL ? MONTH), lastupdate = CURRENT_TIMESTAMP(3), clid = ?, upid = ?, trdate = CURRENT_TIMESTAMP(3), trstatus = 'clientApproved', acdate = CURRENT_TIMESTAMP(3), transfer_exdate = NULL, rgpstatus = 'transferPeriod', transferPeriod = ? WHERE id = ?"); - $stmt->execute([$date_add, $row["reid"], $clid, $date_add, $domain_id]); + $stmt = $db->prepare("UPDATE domain SET exdate = DATE_ADD(exdate, INTERVAL ? MONTH), lastupdate = CURRENT_TIMESTAMP(3), clid = ?, upid = ?, registrant = ?, trdate = CURRENT_TIMESTAMP(3), trstatus = 'clientApproved', acdate = CURRENT_TIMESTAMP(3), transfer_exdate = NULL, rgpstatus = 'transferPeriod', transferPeriod = ? WHERE id = ?"); + $stmt->execute([$date_add, $row["reid"], $newRegistrantId, $clid, $date_add, $domain_id]); + + $new_authinfo = generateAuthInfo(); + $stmt = $db->prepare("UPDATE domain_authInfo SET authinfo = ? WHERE domain_id = ?"); + $stmt->execute([$new_authinfo, $domain_id]); + + foreach ($contactMap as $contact) { + // Construct the SQL update query + $sql = "UPDATE domain_contact_map SET contact_id = :new_contact_id WHERE domain_id = :domain_id AND type = :type AND contact_id = :contact_id"; + + // Prepare the SQL statement + $stmt = $db->prepare($sql); + + // Bind the values to the placeholders + $stmt->bindValue(':new_contact_id', $newContactIds[$contact['contact_id']]); + $stmt->bindValue(':domain_id', $domain_id); + $stmt->bindValue(':type', $contact['type']); + $stmt->bindValue(':contact_id', $contact['contact_id']); + + // Execute the update statement + $stmt->execute(); + } $stmt = $db->prepare("UPDATE host SET clid = ?, upid = ?, lastupdate = CURRENT_TIMESTAMP(3), trdate = CURRENT_TIMESTAMP(3) WHERE domain_id = ?"); $stmt->execute([$row["reid"], $clid, $domain_id]); diff --git a/epp/src/helpers.php b/epp/src/helpers.php index 36a6998..77dda8a 100644 --- a/epp/src/helpers.php +++ b/epp/src/helpers.php @@ -576,4 +576,34 @@ function getDomainPrice($pdo, $domain_name, $tld_id, $date_add = 12, $command = } return ['type' => 'not_found', 'price' => 0]; +} + +function generateAuthInfo(): string { + $length = 16; + $charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + $retVal = ""; + $digitCount = 0; + + // Generate initial random string + for ($i = 0; $i < $length; $i++) { + $randomIndex = random_int(0, strlen($charset) - 1); + $char = $charset[$randomIndex]; + $retVal .= $char; + if ($char >= '0' && $char <= '9') { + $digitCount++; + } + } + + // Ensure there are at least two digits in the string + while ($digitCount < 2) { + // Replace a non-digit character at a random position with a digit + $replacePosition = random_int(0, $length - 1); + if (!($retVal[$replacePosition] >= '0' && $retVal[$replacePosition] <= '9')) { + $randomDigit = random_int(0, 9); // Generate a digit from 0 to 9 + $retVal = substr_replace($retVal, (string)$randomDigit, $replacePosition, 1); + $digitCount++; + } + } + + return $retVal; } \ No newline at end of file