Skip to content

[Security] SSRF via External DAV Storage — Bypass of preventLocalAddress Protection #62725

Description

@leanworld7-netizen

SSRF via External DAV Storage — Bypass of preventLocalAddress Protection

Summary

Nextcloud external DAV storage backend (OC\Files\Storage\DAV) uses Sabre\DAV\Client (raw cURL) for all HTTP requests, completely bypassing the preventLocalAddress() SSRF protection that guards all other HTTP client usage in Nextcloud. An authenticated user with permission to create external storages can configure a DAV storage pointing to an internal address (127.0.0.1, 169.254.169.254, 10.x.x.x) and read the response through the filesystem interface.

Affected Component

  • File: lib/private/Files/Storage/DAV.php
  • Class: OC\Files\Storage\DAV
  • Client: BearerAuthAwareSabreClient extends Sabre\DAV\Client (uses raw cURL via CURLOPT_*)

Vulnerability Description

All other HTTP clients in Nextcloud go through OC\Http\Client.Client which calls preventLocalAddress() on every request. This uses IRemoteHostValidator::isValid() to check the host against IpAddressClassifier and HostnameClassifier, blocking connections to localhost, private ranges, and link-local addresses.

However, the DAV external storage backend instantiates BearerAuthAwareSabreClient (which extends Sabre\DAV\Client) directly:

// DAV.php L273
$this->client = new BearerAuthAwareSabreClient($settings);

Sabre\DAV\Client uses PHP cURL extension directly. It does NOT call preventLocalAddress(), IRemoteHostValidator::isValid(), or any equivalent check. The host parameter from storage configuration flows directly to the cURL handle:

// DAV.php L189-197
$host = $parameters['host'];
if (str_starts_with($host, 'https://')) {
    $host = substr($host, 8);
} elseif (str_starts_with($host, 'http://')) {
    $host = substr($host, 7);
}
$this->host = $host;

No validation is performed on the host value. There is no call to preventLocalAddress, remoteHostValidator, isValid, filter, sanitize, escape, or FILTER_VALIDATE anywhere in DAV.php.

The base URI is constructed in createBaseUri() and used for all PROPFIND/GET/PUT requests via Sabre cURL client.

Attack Scenario

  1. Attacker creates external storage of type WebDAV via files_external API
  2. Host parameter set to internal address (169.254.169.254 for AWS metadata, 127.0.0.1:8080 for internal services, 10.0.0.1 for internal network)
  3. When storage is accessed (directory listing), Nextcloud makes PROPFIND request to internal address via cURL
  4. Response from internal service is returned through filesystem interface, allowing attacker to read internal service responses, cloud metadata, or other internal resources

Impact

  • Authenticated SSRF: Any user who can create external storages can make arbitrary requests to internal services
  • Information disclosure: Internal service responses (including cloud provider metadata endpoints) are readable through the filesystem interface
  • Network scanning: By testing different hosts/ports, attacker can enumerate internal network topology
  • Bypass of security controls: The preventLocalAddress() guard that protects all other Nextcloud HTTP clients is completely bypassed

Prerequisites

  • Authenticated Nextcloud user with permission to create external storages
  • If allow_user_mounting is disabled (default), admin access is required
  • If allow_user_mounting is enabled, any authenticated user can create DAV external storages

Remediation

Add preventLocalAddress() check (or equivalent IRemoteHostValidator::isValid() call) before creating the Sabre DAV client in DAV.php init(), and validate the host parameter in the constructor.

References

  • lib/private/Http/Client/Client.php L168 — preventLocalAddress() implementation
  • lib/private/Security/RemoteHostValidator.php — isValid() method
  • lib/private/Net/IpAddressClassifier.php — IP range blocking
  • lib/private/Net/HostnameClassifier.php — hostname blocking

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status
    To triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions