From ee1572155066eeebac6af03958a32ae59d18f5f7 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 17 Dec 2025 13:39:05 +0100 Subject: [PATCH 1/5] feat: implement passwordless autodiscover endpoint - Remove HTTP Basic Authentication requirement from autodiscover.php - Extract email address from XML request body instead of AUTH headers - Validate mailbox existence and active status before returning config - Improve security by eliminating password transmission - Add comprehensive error handling for invalid/inactive mailboxes - Follow industry standards (Microsoft, Google, Apple) - Maintain backward compatibility with existing email clients - Keep full logging functionality in Redis AUTODISCOVER_LOG This change enhances security while improving user experience and follows modern email client configuration best practices. --- data/web/autodiscover.php | 215 +++++++++++++++++++++++--------------- 1 file changed, 130 insertions(+), 85 deletions(-) diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php index 224f94f71..fe1e8e910 100644 --- a/data/web/autodiscover.php +++ b/data/web/autodiscover.php @@ -60,97 +60,25 @@ $pdo = new PDO($dsn, $database_user, $database_pass, $opt); $iam_provider = identity_provider('init'); $iam_settings = identity_provider('get'); -$login_user = strtolower(trim($_SERVER['PHP_AUTH_USER'])); -$login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW'])); +// Passwordless autodiscover - no authentication required +// Email will be extracted from the request body +$login_user = null; +$login_role = null; -if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) { - $json = json_encode( - array( - "time" => time(), - "ua" => $_SERVER['HTTP_USER_AGENT'], - "user" => "none", - "ip" => $_SERVER['REMOTE_ADDR'], - "service" => "Error: must be authenticated" - ) - ); - $redis->lPush('AUTODISCOVER_LOG', $json); - header('WWW-Authenticate: Basic realm="' . $_SERVER['HTTP_HOST'] . '"'); - header('HTTP/1.0 401 Unauthorized'); - exit(0); -} - -$login_role = check_login($login_user, $login_pass, array('eas' => TRUE)); - -if ($login_role === "user") { - header("Content-Type: application/xml"); - echo '' . PHP_EOL; +header("Content-Type: application/xml"); +echo '' . PHP_EOL; ?> time(), - "ua" => $_SERVER['HTTP_USER_AGENT'], - "user" => $_SERVER['PHP_AUTH_USER'], - "ip" => $_SERVER['REMOTE_ADDR'], - "service" => "Error: invalid or missing request data" - ) - ); - $redis->lPush('AUTODISCOVER_LOG', $json); - $redis->lTrim('AUTODISCOVER_LOG', 0, 100); - } - catch (RedisException $e) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'msg' => 'Redis: '.$e - ); - return false; - } - list($usec, $sec) = explode(' ', microtime()); -?> - - - 600 - Invalid Request - - - - -Request->EMailAddress; - } catch (Exception $e) { - $email = $_SERVER['PHP_AUTH_USER']; - } - - $username = trim($email); - try { - $stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); - $stmt->execute(array(':username' => $username)); - $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - die("Failed to determine name from SQL"); - } - if (!empty($MailboxData['name'])) { - $displayname = $MailboxData['name']; - } - else { - $displayname = $email; - } +if(!$data) { try { $json = json_encode( array( "time" => time(), "ua" => $_SERVER['HTTP_USER_AGENT'], - "user" => $_SERVER['PHP_AUTH_USER'], + "user" => "none", "ip" => $_SERVER['REMOTE_ADDR'], - "service" => $autodiscover_config['autodiscoverType'] + "service" => "Error: invalid or missing request data" ) ); $redis->lPush('AUTODISCOVER_LOG', $json); @@ -163,7 +91,127 @@ if ($login_role === "user") { ); return false; } - if ($autodiscover_config['autodiscoverType'] == 'imap') { + list($usec, $sec) = explode(' ', microtime()); +?> + + + 600 + Invalid Request + + + + +Request->EMailAddress; +} catch (Exception $e) { + // If parsing fails, return error + try { + $json = json_encode( + array( + "time" => time(), + "ua" => $_SERVER['HTTP_USER_AGENT'], + "user" => "none", + "ip" => $_SERVER['REMOTE_ADDR'], + "service" => "Error: could not parse email from request" + ) + ); + $redis->lPush('AUTODISCOVER_LOG', $json); + $redis->lTrim('AUTODISCOVER_LOG', 0, 100); + } + catch (RedisException $e) { + // Silently fail + } + list($usec, $sec) = explode(' ', microtime()); +?> + + + 600 + Invalid Request + + + + +prepare("SELECT `name`, `active` FROM `mailbox` + INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain` + WHERE `mailbox`.`username` = :username + AND `mailbox`.`active` = '1' + AND `domain`.`active` = '1'"); + $stmt->execute(array(':username' => $username)); + $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); +} +catch(PDOException $e) { + die("Failed to determine name from SQL"); +} + +// Mailbox not found or not active - return error +if (empty($MailboxData)) { + try { + $json = json_encode( + array( + "time" => time(), + "ua" => $_SERVER['HTTP_USER_AGENT'], + "user" => $email, + "ip" => $_SERVER['REMOTE_ADDR'], + "service" => "Error: mailbox not found or inactive" + ) + ); + $redis->lPush('AUTODISCOVER_LOG', $json); + $redis->lTrim('AUTODISCOVER_LOG', 0, 100); + } + catch (RedisException $e) { + // Silently fail + } + list($usec, $sec) = explode(' ', microtime()); +?> + + + 600 + Mailbox not found + + + + + time(), + "ua" => $_SERVER['HTTP_USER_AGENT'], + "user" => $email, + "ip" => $_SERVER['REMOTE_ADDR'], + "service" => $autodiscover_config['autodiscoverType'] + ) + ); + $redis->lPush('AUTODISCOVER_LOG', $json); + $redis->lTrim('AUTODISCOVER_LOG', 0, 100); +} +catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; +} +if ($autodiscover_config['autodiscoverType'] == 'imap') { ?> @@ -238,6 +286,3 @@ if ($login_role === "user") { } ?> - From ec77406dba452270fdc24b38b2d8e7e2b3587f97 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 17 Dec 2025 14:27:38 +0100 Subject: [PATCH 2/5] Fix autodiscover.php: Use random error IDs and fix SQL type casting - Replace hardcoded error IDs with random values (1-10 billion range) for better debugging - Cast SimpleXMLElement email to string before SQL query to prevent type errors - Qualify ambiguous 'active' column with table names in JOIN query - Add proper error XML response for database errors instead of die() - Ensure all error paths return complete XML documents --- data/web/autodiscover.php | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php index fe1e8e910..e5d159815 100644 --- a/data/web/autodiscover.php +++ b/data/web/autodiscover.php @@ -94,7 +94,7 @@ if(!$data) { list($usec, $sec) = explode(' ', microtime()); ?> - + 600 Invalid Request @@ -128,7 +128,7 @@ try { list($usec, $sec) = explode(' ', microtime()); ?> - + 600 Invalid Request @@ -139,9 +139,9 @@ try { exit(0); } -$username = trim($email); +$username = trim((string)$email); try { - $stmt = $pdo->prepare("SELECT `name`, `active` FROM `mailbox` + $stmt = $pdo->prepare("SELECT `mailbox`.`name`, `mailbox`.`active` FROM `mailbox` INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain` WHERE `mailbox`.`username` = :username AND `mailbox`.`active` = '1' @@ -150,7 +150,19 @@ try { $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); } catch(PDOException $e) { - die("Failed to determine name from SQL"); + // Database error - return error response with complete XML + list($usec, $sec) = explode(' ', microtime()); +?> + + + 500 + Database Error + + + + + - + 600 Mailbox not found From b005803fe03a0036c1942303fd94a8014fe928ed Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 17 Dec 2025 14:27:53 +0100 Subject: [PATCH 3/5] Add autodiscover debug script with domain override support - Add view_autodiscover.sh helper script for testing autodiscover responses - Support -h/--help flag for usage information - Support -d/--domain flag to override autodiscover target (useful for testing) - Auto-detect xmllint availability for formatted output - Email validation with regex - Interactive mode if no email provided - Display response length for debugging --- helper-scripts/dev_tests/view_autodiscover.sh | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100755 helper-scripts/dev_tests/view_autodiscover.sh diff --git a/helper-scripts/dev_tests/view_autodiscover.sh b/helper-scripts/dev_tests/view_autodiscover.sh new file mode 100755 index 000000000..a203370de --- /dev/null +++ b/helper-scripts/dev_tests/view_autodiscover.sh @@ -0,0 +1,122 @@ +#!/bin/bash + +# Autodiscover XML Debug Script +# Usage: ./view_autodiscover.sh [OPTIONS] [email@domain.com] + +# Function to display help +show_help() { + cat << EOF +Autodiscover XML Debug Script + +Usage: $0 [OPTIONS] [email@domain.com] + +OPTIONS: + -h, --help Show this help message + -d, --domain FQDN Override autodiscover domain (default: autodiscover.DOMAIN) + Example: -d mail.example.com + +EXAMPLES: + $0 user@example.com + Test autodiscover for user@example.com using autodiscover.example.com + + $0 -d mail.example.com user@example.com + Test autodiscover for user@example.com using mail.example.com + + $0 -d localhost:8443 user@example.com + Test autodiscover using localhost:8443 (useful for development) + +EOF + exit 0 +} + +# Initialize variables +EMAIL="" +DOMAIN_OVERRIDE="" + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + ;; + -d|--domain) + DOMAIN_OVERRIDE="$2" + shift 2 + ;; + -*) + echo "Error: Unknown option $1" + echo "Use -h or --help for usage information" + exit 1 + ;; + *) + EMAIL="$1" + shift + ;; + esac +done + +# Check if xmllint is available +if ! command -v xmllint &> /dev/null; then + echo "WARNING: xmllint not found. Output will not be formatted." + echo "Install with: apt install libxml2-utils (Debian/Ubuntu) or yum install libxml2 (CentOS/RHEL)" + echo "" + USE_XMLLINT=false +else + USE_XMLLINT=true +fi + +# Get email address from user input if not provided +if [ -z "$EMAIL" ]; then + read -p "Enter email address to test: " EMAIL +fi + +# Validate email format +if [[ ! "$EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then + echo "Error: Invalid email address format" + exit 1 +fi + +# Extract domain from email +EMAIL_DOMAIN="${EMAIL#*@}" + +# Determine autodiscover URL +if [ -n "$DOMAIN_OVERRIDE" ]; then + AUTODISCOVER_URL="https://${DOMAIN_OVERRIDE}/Autodiscover/Autodiscover.xml" + echo "Testing Autodiscover for: $EMAIL" + echo "Override domain: $DOMAIN_OVERRIDE" +else + AUTODISCOVER_URL="https://autodiscover.${EMAIL_DOMAIN}/Autodiscover/Autodiscover.xml" + echo "Testing Autodiscover for: $EMAIL" +fi + +echo "URL: $AUTODISCOVER_URL" +echo "============================================" +echo "" + +# Make the request +RESPONSE=$(curl -k -s -X POST "$AUTODISCOVER_URL" \ + -H "Content-Type: text/xml" \ + -d " + + + $EMAIL + http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a + +") + +# Check if response is empty +if [ -z "$RESPONSE" ]; then + echo "Error: No response received from server" + exit 1 +fi + +# Format and display output +if [ "$USE_XMLLINT" = true ]; then + echo "$RESPONSE" | xmllint --format - 2>&1 +else + echo "$RESPONSE" +fi + +echo "" +echo "============================================" +echo "Response length: ${#RESPONSE} bytes" From 5ca900749cb4dcea3b024e51c4f35169ab9e2290 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Thu, 18 Dec 2025 16:54:45 +0100 Subject: [PATCH 4/5] autodiscover: use generalized error logging instead of specific to prevent user enumeration --- data/web/autodiscover.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php index e5d159815..c0e69537e 100644 --- a/data/web/autodiscover.php +++ b/data/web/autodiscover.php @@ -165,7 +165,7 @@ catch(PDOException $e) { exit(0); } -// Mailbox not found or not active - return error +// Mailbox not found or not active - return generic error to prevent user enumeration if (empty($MailboxData)) { try { $json = json_encode( @@ -188,7 +188,7 @@ if (empty($MailboxData)) { 600 - Mailbox not found + Invalid Request From af61e2d3033c070040703982ba9fda732e0f2e7d Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:24:44 +0100 Subject: [PATCH 5/5] [Web] Add fail2ban logging to passwordless autodiscover endpoint --- data/web/autodiscover.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php index c0e69537e..3e31dc3dd 100644 --- a/data/web/autodiscover.php +++ b/data/web/autodiscover.php @@ -83,6 +83,8 @@ if(!$data) { ); $redis->lPush('AUTODISCOVER_LOG', $json); $redis->lTrim('AUTODISCOVER_LOG', 0, 100); + $redis->publish("F2B_CHANNEL", "Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']); + error_log("Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']); } catch (RedisException $e) { $_SESSION['return'][] = array( @@ -121,6 +123,8 @@ try { ); $redis->lPush('AUTODISCOVER_LOG', $json); $redis->lTrim('AUTODISCOVER_LOG', 0, 100); + $redis->publish("F2B_CHANNEL", "Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']); + error_log("Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']); } catch (RedisException $e) { // Silently fail @@ -179,6 +183,8 @@ if (empty($MailboxData)) { ); $redis->lPush('AUTODISCOVER_LOG', $json); $redis->lTrim('AUTODISCOVER_LOG', 0, 100); + $redis->publish("F2B_CHANNEL", "Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']); + error_log("Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']); } catch (RedisException $e) { // Silently fail