Skip to content

fix(activesync): harden auth header, Autodiscover parsing/XML escaping, and ValidateCert#74

Merged
TDannhauer merged 4 commits into
FRAMEWORK_6_0from
security/activesync-hardening
Jul 2, 2026
Merged

fix(activesync): harden auth header, Autodiscover parsing/XML escaping, and ValidateCert#74
TDannhauer merged 4 commits into
FRAMEWORK_6_0from
security/activesync-hardening

Conversation

@TDannhauer

Copy link
Copy Markdown
Contributor

Summary

  • Harden HTTP Basic auth header parsing in the ActiveSync Credentials handler.
  • Parse the Autodiscover EMailAddress by tag name and XML-escape all values interpolated into Autodiscover responses.
  • Correct the ValidateCert OpenSSL result handling (dead/typo'd error branch).

Motivation

A read-through of the ActiveSync request path surfaced a few low-level robustness and content-injection issues in the parts of the library that consume untrusted client input (the Authorization header, the Autodiscover request body, and S/MIME certificate validation).

Changes

  • Credentials.php — Strip only a leading, case-insensitive Basic scheme token (str_replace() removed the token anywhere in the string) and decode strictly (base64_decode(..., true)), so malformed Authorization headers produce no credentials instead of arbitrary bytes.
  • Request/Autodiscover.php
    • Locate the EMailAddress element by tag name instead of the fixed offset $values[2], which selected the wrong node (or errored) when a client reordered/added elements. The wire value is always an email address per the Autodiscover protocol; mapping it to a backend username stays the driver's responsibility (getUsernameFromEmail()), so this is backend-agnostic (email-as-username, AD/LDAP, plain usernames).
    • Add an _xmlEscape() helper (ENT_QUOTES | ENT_XML1) and apply it to every value interpolated into the mobilesync/outlook/failure responses — including the client-supplied request/response schemas echoed back into XML attributes — closing a content-injection/XML-corruption gap.
  • Request/ValidateCert.phpopenssl_x509_checkpurpose() returns true/false/-1. The -1 branch tested an undefined $results variable (typo for $result) and was dead code; reorder to check === -1 first so certificate errors are reported as unknown rather than misclassified as an invalid purpose.

Test plan

  • php -l clean on all changed files
  • AutodiscoverTest (4/4) and ServerTest (2/2) pass
  • Manual Autodiscover round-trip against a device/emulator
  • Manual ValidateCert with a valid, expired, and untrusted S/MIME certificate

Strip only a leading, case-insensitive "Basic " scheme token instead of
str_replace(), which removed the token anywhere in the string, and decode
strictly so malformed Authorization headers yield no credentials rather
than arbitrary bytes.
Locate the Autodiscover EMailAddress element by tag name instead of a
fixed offset ($values[2]), which selected the wrong node (or errored) when
a client reordered or added elements. The wire value is always an email
address; mapping it to a backend username remains the driver's job.

XML-escape every value interpolated into the Autodiscover responses
(email, display name, URLs, backend host/port, and the client-supplied
request/response schemas echoed back into attributes). These were
previously concatenated raw, corrupting the XML for values containing
metacharacters and allowing content injection via the schema values.
openssl_x509_checkpurpose() returns true, false, or -1 on error. The -1
branch tested an undefined $results variable (typo for $result) and was
dead code; reorder to check the -1 error case first with a strict
comparison so certificate errors are reported as unknown rather than
misclassified as an invalid purpose.
@TDannhauer TDannhauer requested a review from ralflang July 2, 2026 06:29
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

🔍 CI Results

Overall: ❌ 12/12 lanes failed

TL;DR: ❌ Quality issues: PHPUnit: 36 tests failed; PHPStan: 332 unique errors in 8 lanes.

Summary by PHP Version

PHP dev stable
8.0
8.1
8.2
8.3
8.4
8.5

Quality Metrics

  • PHPUnit: 16 failures, 20 errors out of 199 tests in 8 lanes ❌
    Per-test detail in the table below. Raw JUnit XML and JSON are uploaded as workflow artifacts.

    Which tests failed
    Type Test Lanes Message
    ❌ failure Horde\ActiveSync\AppointmentTest::testSetRecurrenceEas16SetsFirstDayOfWeek php8.2-dev, php8.2-stable, php8.3-dev, php8.3-stable, php8.4-dev, php8.4-stable, php8.5-dev, php8.5-stable [PHPUnit\Framework\ExpectationFailedException] Failed asserting that '' matches expected 0.
    ⚠️ error Horde\ActiveSync\Request\FindRequestTest::testMockDriverReturnsSuccessfulEmptyFindResults php8.2-dev, php8.2-stable, php8.3-dev, php8.3-stable, php8.4-dev, php8.4-stable, php8.5-dev, php8.5-stable [InvalidArgumentException] Missing required state object
    ❌ failure Horde\ActiveSync\Request\FindRequestTest::testParseFindRangeHonorsClientWindow php8.2-dev, php8.2-stable, php8.3-dev, php8.3-stable, php8.4-dev, php8.4-stable, php8.5-dev, php8.5-stable [PHPUnit\Framework\ExpectationFailedException] Failed asserting that 100 is identical to 101.
    ⚠️ error Horde\ActiveSync\SyncTimeBudgetTest::testGetSyncConfigDefaultsToEmptyArray php8.3-dev, php8.3-stable, php8.4-dev, php8.4-stable, php8.5-dev, php8.5-stable [Error] Call to undefined method PHPUnit\Framework\MockObject\MockBuilder::getMockForAbstractClass()
    ⚠️ error Horde\ActiveSync\SyncTimeBudgetTest::testGetSyncConfigReturnsConfiguredValues php8.3-dev, php8.3-stable, php8.4-dev, php8.4-stable, php8.5-dev, php8.5-stable [Error] Call to undefined method PHPUnit\Framework\MockObject\MockBuilder::getMockForAbstractClass()
  • PHPStan (advisory): 332 unique errors in 8 lanes ⚠️

  • PHP-CS-Fixer: did not run on any lane ❌

❌ Failed Lanes

php8.0-dev

  • PHPUnit: ❌ Setup failed (No result file found)
  • PHPStan: ❌ Setup failed (No result file found)

php8.0-stable

  • PHPUnit: ❌ Setup failed (Composer install failed in /tmp/horde-ci/lanes/php8.0-stable/ActiveSync: Your requirements could not be resolved to an installable set of packages.)
  • PHPStan: ❌ Setup failed (Composer install failed in /tmp/horde-ci/lanes/php8.0-stable/ActiveSync: Your requirements could not be resolved to an installable set of packages.)

php8.1-dev

  • PHPUnit: ❌ Setup failed (No result file found)
  • PHPStan: ❌ Setup failed (No result file found)

php8.1-stable

  • PHPUnit: ❌ Setup failed (No result file found)
  • PHPStan: ❌ Setup failed (No result file found)

php8.2-dev

  • PHPUnit: 2 failures, 1 error

php8.2-stable

  • PHPUnit: 2 failures, 1 error

php8.3-dev

  • PHPUnit: 2 failures, 3 errors

php8.3-stable

  • PHPUnit: 2 failures, 3 errors

php8.4-dev

  • PHPUnit: 2 failures, 3 errors
  • PHP-CS-Fixer: ❌ Setup failed (No result file found)

php8.4-stable

  • PHPUnit: 2 failures, 3 errors

php8.5-dev

  • PHPUnit: 2 failures, 3 errors

php8.5-stable

  • PHPUnit: 2 failures, 3 errors

CI powered by horde-componentsView full results

@ralflang ralflang left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see inline comment

Comment thread lib/Horde/ActiveSync/Request/ValidateCert.php
Record the PR #74 review follow-up: certificate validation should become a
validator interface owned by Horde\ActiveSync with the implementation and
configuration owned by Core, rather than the inline OpenSSL checks in
Request_ValidateCert.
@TDannhauer TDannhauer merged commit 4d078e7 into FRAMEWORK_6_0 Jul 2, 2026
1 check failed
ralflang added a commit that referenced this pull request Jul 4, 2026
Release version 3.0.1

fix(activesync): treat collection lock contention as transient during PING
Merge pull request #74 from horde/security/activesync-hardening
docs(activesync): note ValidateCert validator extraction in H6 todo
fix(activesync): correct ValidateCert OpenSSL result handling
fix(activesync): harden Autodiscover parsing and escape XML responses
fix(activesync): harden HTTP Basic auth header parsing
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants