From cdc18040b83bc1f443356499c727f2b316441a5f Mon Sep 17 00:00:00 2001 From: Torben Dannhauer Date: Wed, 1 Jul 2026 12:01:36 +0200 Subject: [PATCH] fix(activesync): harden base64 URI decoding for GET requests Reject malformed QUERY_STRING payloads before unpack() instead of logging PHP warnings for unknown commands and truncated device IDs. Add missing EAS command codes and skip DeviceType normalization when decode returns no Cmd. --- lib/Horde/ActiveSync.php | 5 +- lib/Horde/ActiveSync/Utils.php | 65 +++++++++++++++++++++--- test/unit/Horde/ActiveSync/UtilsTest.php | 6 +++ 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/lib/Horde/ActiveSync.php b/lib/Horde/ActiveSync.php index 2aa32f59..defd2f4f 100644 --- a/lib/Horde/ActiveSync.php +++ b/lib/Horde/ActiveSync.php @@ -1207,8 +1207,11 @@ public function getGetVars() $serverVars = $this->_request->getServerVars(); if (isset($serverVars['QUERY_STRING']) && strlen($serverVars['QUERY_STRING']) >= 10) { $results = Horde_ActiveSync_Utils::decodeBase64($serverVars['QUERY_STRING']); + if (empty($results['Cmd'])) { + return $this->_get; + } // Normalize values. - switch ($results['DeviceType']) { + switch ($results['DeviceType'] ?? '') { case 'PPC': $results['DeviceType'] = 'PocketPC'; break; diff --git a/lib/Horde/ActiveSync/Utils.php b/lib/Horde/ActiveSync/Utils.php index 2cb13373..ade78a86 100644 --- a/lib/Horde/ActiveSync/Utils.php +++ b/lib/Horde/ActiveSync/Utils.php @@ -37,6 +37,10 @@ public static function decodeBase64($uri) 2 => 'SmartForward', 3 => 'SmartReply', 4 => 'GetAttachment', + 5 => 'GetHierarchy', + 6 => 'CreateCollection', + 7 => 'DeleteCollection', + 8 => 'MoveCollection', 9 => 'FolderSync', 10 => 'FolderCreate', 11 => 'FolderDelete', @@ -51,36 +55,83 @@ public static function decodeBase64($uri) 20 => 'Provision', 21 => 'ResolveRecipients', 22 => 'ValidateCert', + 23 => 'Find', ]; + + $decoded = base64_decode($uri, true); + if ($decoded === false || strlen($decoded) < 4) { + return []; + } + $stream = fopen('php://temp', 'r+'); - fwrite($stream, base64_decode($uri)); + fwrite($stream, $decoded); rewind($stream); $results = []; + // Version, command, locale - $data = unpack('CprotocolVersion/Ccommand/vlocale', fread($stream, 4)); + $header = fread($stream, 4); + if (strlen($header) < 4) { + return []; + } + $data = unpack('CprotocolVersion/Ccommand/vlocale', $header); + if (!isset($commandMap[$data['command']])) { + return []; + } $results['ProtVer'] = substr($data['protocolVersion'], 0, -1) . '.' . substr($data['protocolVersion'], -1); $results['Cmd'] = $commandMap[$data['command']]; $results['Locale'] = $data['locale']; // deviceId - $length = ord(fread($stream, 1)); + $lengthByte = fread($stream, 1); + if ($lengthByte === '' || $lengthByte === false) { + return $results; + } + $length = ord($lengthByte); if ($length > 0) { $data = fread($stream, $length); + if (strlen($data) < $length) { + return []; + } $data = unpack('H' . ($length * 2) . 'DevID', $data); + if ($data === false || !isset($data['DevID'])) { + return []; + } $results['DeviceId'] = $data['DevID']; } // policyKey - $length = ord(fread($stream, 1)); + $lengthByte = fread($stream, 1); + if ($lengthByte === '' || $lengthByte === false) { + return $results; + } + $length = ord($lengthByte); if ($length > 0) { - $data = unpack('VpolicyKey', fread($stream, $length)); + $data = fread($stream, $length); + if (strlen($data) < $length) { + return []; + } + $data = unpack('VpolicyKey', $data); + if ($data === false || !isset($data['policyKey'])) { + return []; + } $results['PolicyKey'] = $data['policyKey']; } // deviceType - $length = ord(fread($stream, 1)); + $lengthByte = fread($stream, 1); + if ($lengthByte === '' || $lengthByte === false) { + return $results; + } + $length = ord($lengthByte); if ($length > 0) { - $data = unpack('A' . $length . 'devType', fread($stream, $length)); + $data = fread($stream, $length); + if (strlen($data) < $length) { + return []; + } + $data = unpack('A' . $length . 'devType', $data); + if ($data === false || !isset($data['devType'])) { + return []; + } $results['DeviceType'] = $data['devType']; } diff --git a/test/unit/Horde/ActiveSync/UtilsTest.php b/test/unit/Horde/ActiveSync/UtilsTest.php index 14b9d227..b0200e5f 100644 --- a/test/unit/Horde/ActiveSync/UtilsTest.php +++ b/test/unit/Horde/ActiveSync/UtilsTest.php @@ -55,6 +55,12 @@ public function testBase64Uri() $this->assertEquals($fixture, $results); } + public function testDecodeBase64RejectsInvalidPayload() + { + $this->assertEquals([], Horde_ActiveSync_Utils::decodeBase64('not-valid-eas-data')); + $this->assertEquals([], Horde_ActiveSync_Utils::decodeBase64('YWJj')); + } + public function testBodyTypePref() { $this->markTestIncomplete('Needs refactoring.');