nextcloud / nextcloud/server

Align Sabre DAV Client with IClient SSRF Protection for External WebDAV

Open
#62,725 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

1. to develop 34-feedback bug feature: external storage technical debt
Dominant language
PHP
Stars
36.9k
Forks
5.2k
Avg merge
2d 3h
Merged PRs (30d)
713

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with lib/private/Files/Storage/DAV.php, especially init() and the BearerAuthAwareSabreClient construction, then compare lib/private/Http/Client/Client.php with RemoteHostValidator.php, IpAddressClassifier.php, and HostnameClassifier.php. Done means DAV host validation is applied before requests reach Sabre's cURL client and local, private, and link-local targets are blocked.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.