Skip to content

DAV Basic Auth: empty username/password reaches getByEmail and scans oc_preferences (heavy on NC 31/32) #59849

Description

@joaoathaide

DAV Basic Auth: empty username/password reaches getByEmail and scans oc_preferences (heavy on NC 31/32)

How to reproduce

  1. Run a WebDAV request against a private calendar/addressbook URL with Basic authentication header where both username and password are empty (e.g. Thunderbird or other clients sending Authorization: Basic Og==).

Example:

curl -X PROPFIND \
  -H 'Authorization: Basic Og==' \
  -H 'Content-Type: text/xml; charset=utf-8' \
  -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Thunderbird/140.9.0' \
  -d '<?xml version="1.0" encoding="UTF-8"?><D:propfind xmlns:D="DAV:"><D:prop><D:resourcetype/></D:prop></D:propfind>' \
  'https://<host>/remote.php/dav/calendars/<user>/personal/'

Expected behaviour

The request should fail fast as unauthenticated without running a broad database query over user preferences (equivalent to “no credentials provided”).

Actual behaviour

The Sabre OCA\DAV\Connector\Sabre\Auth backend still proceeds far enough that Session::logClientIn() runs with an empty login name. That path calls isTwoFactorEnforced($user) with an empty/null-like username. Inside isTwoFactorEnforced, IUserManager::get() fails and the code falls through to getByEmail($username) with an empty string.

User\Manager::getByEmail() resolves users via getUsersForUserValueCaseInsensitive('settings', 'email', $email), which builds a query on preferences. On instances where UserConfig::isUpgradedTo31() is true (Nextcloud 31.x and 32.x), searchUsersByTypedValue() adds the indexed/configvalue OR logic, producing SQL similar to:

SELECT `userid` FROM `oc_preferences`
WHERE (`appid` = 'settings')
  AND (`configkey` = 'email')
  AND ((`indexed` = '') OR (((`flags` & 2) <> 2) AND (`configvalue` = '')))

That effectively scans users matching “empty email” storage, which is very expensive on large instances and is triggered by a client sending empty Basic credentials.

On 33+, isUpgradedTo31() is false (only 31.* and 32.*), so the query shape differs and this specific heavy path may not apply — but the logical bug (treating empty credentials like a resolvable email lookup) likely remains worth fixing centrally.

Analysis / call chain (summary)

  1. apps/dav/lib/Connector/Sabre/Auth.phpvalidateUserPass($username, $password) can call logClientIn even when both are empty.
  2. lib/private/User/Session.phplogClientInisTwoFactorEnforced($user) before password validation.
  3. isTwoFactorEnforcedgetByEmail($username) when user is null.
  4. lib/private/User/Manager.php::getByEmail → preference search.
  5. lib/private/Config/UserConfig.php::searchUsersByTypedValue — expensive branch when isUpgradedTo31() is true.

Proposed fix (one option)

Early-return in DAV Basic auth when both username and password are empty, e.g. in Auth::validateUserPass: close session and return false without calling logClientIn. This matches the idea that Basic auth with no credentials should not enter the full login/2FA resolution stack.

Alternatively (or additionally), guard in Session::isTwoFactorEnforced / getByEmail against null/empty email so preference search is never run for an empty lookup value.

Server configuration

  • Affected: Nextcloud 31.x, 32.x (confirmed heavy query path via isUpgradedTo31()).
  • Client example: Thunderbird 140.x sending Basic Og== on PROPFIND to CalDAV.

Additional context

We validated locally that rejecting empty user+password at the DAV auth layer returns a normal authentication failure to the client without triggering the preference scan.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions