[Bug]: DAV Forbidden/InvalidPath responses break on "&" in the message (DOMDocument::createElementNS(): unterminated entity reference)

オープン 初心者向け
#66,138 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@th-joerger がすでに取り組んでいます。

2026年11月2日 から。

  • #66139 @th-joerger による — オープン

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
72/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
php, xml
領域
backend

調査の方向性

問題は apps/dav/lib/Connector/Sabre/Exception/Forbidden.php の46行目付近にある serialize() にあります。ここでは createElementNS の値引数としてメッセージを o:reason に渡しています。同じパターンが InvalidPath.php の58行目付近にもあり、Server.php の247行目が s:message の正しいエスケープ方法を示しています。まず issue の短い PHP スニペットで再現させ、その後両方のクラスを変更して代わりにテキストノードを追加してください。o:reason 要素にエスケープ済みのメッセージが入り、PHP の警告が出ず、両方の例外クラスをユニットテストがカバーしていれば完了です。

索引モデルが issue の本文から書いたものです。

説明

0. Needs triage 35-feedback bug
⚠️ This issue respects the following points: ⚠️
  • This is not a troubleshooting question, general support matter, or webserver/proxy problem, but likely a bug (if unsure, ask the Community Help Forum).
  • This issue is not already reported on Github OR solved at the Community Help Forum (I've searched!).
  • I'm using a maintained major version of Nextcloud Server and tested against the latest patch level. (Supported major versions and current patch levels).
  • I agree to follow Nextcloud's Code of Conduct.
  • I've tried my best to provide clear reproduction steps that someone unfamiliar with this bug could use to reproduce it.
Bug description

When a DAV request fails with OCA\DAV\Connector\Sabre\Exception\Forbidden and the exception message contains an & (e.g. a folder named Muster GmbH & Co. KG), the XML error response is not built correctly. With libxml2 2.9.14 (Debian-based PHP images) PHP logs a warning and <o:reason> is sent empty. With libxml2 2.13.9 (Alpine) there is no warning, but the & is silently dropped from the message.

Cause: Forbidden::serialize() passes the message as the value argument of DOMDocument::createElementNS():

https://github.com/nextcloud/server/blob/0554acf48774bbba73a6315eed5c561303f9e764/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php#L46

$error = $errorNode->ownerDocument->createElementNS('o:', 'o:reason', $this->getMessage());

PHP does not escape & in that argument and treats it as the start of an entity reference (documented in the PHP manual, see also PHP bug #31191, "Won't fix"). The same pattern was reported for ownCloud's bundled Sabre back in ownCloud 4.5.5: owncloud/core#1223. apps/dav/lib/Connector/Sabre/Exception/InvalidPath.php#L58 has the same pattern. apps/dav/lib/Connector/Sabre/Server.php#L247 escapes the same message correctly for <s:message>, so only these two exception classes are affected.

Seen in practice with a desktop client that tried to move a received share into another share. The message comes from lib/private/Files/View.php#L923 (Moving a storage (%s) into a share (%s) is not allowed).

Suggested fix

Create the element empty and append a text node (escapes automatically), in both Forbidden.php and InvalidPath.php. The dav app already does this in apps/dav/lib/Exception/UidConflict.php#L76-L77. Verified with the PHP reproduction above on both libxml2 versions, and end to end on the instance above (both files patched, same steps). <o:reason> then carries the escaped message and no warning is logged:

  <o:reason xmlns:o="o:">Moving a storage (Muster GmbH &amp; Co. KG) into a share (Projekt) is not allowed</o:reason>

A PR with unit tests for both classes follows:

$error = $errorNode->ownerDocument->createElementNS('o:', 'o:reason');
$error->appendChild($errorNode->ownerDocument->createTextNode($this->getMessage()));

Side note: when the root of a received share is moved, View::rename() passes the parent folder's mount as $sourceMount to validateMountMove(), so the message says "Moving a storage" although a share is being moved.

Environment
  • Nextcloud Server: 35.0.2 (Docker, Apache image)
  • Database: MariaDB 11.8
  • PHP: 8.5.12, libxml2 2.9.14
  • Bug also present in current master (checked at 0554acf).
Steps to reproduce

Minimal reproduction in plain PHP, same calls as Forbidden::serialize():

<?php
$msg = 'Moving a storage (Muster GmbH & Co. KG) into a share (Projekt) is not allowed';
$DOM = new DOMDocument('1.0', 'utf-8');
$errorNode = $DOM->createElementNS('DAV:', 'd:error');
$DOM->appendChild($errorNode);

// current code
$old = $errorNode->ownerDocument->createElementNS('o:', 'o:reason', $msg);
echo $DOM->saveXML($old), PHP_EOL;

// proposed fix
$new = $errorNode->ownerDocument->createElementNS('o:', 'o:reason');
$new->appendChild($errorNode->ownerDocument->createTextNode($msg));
echo $DOM->saveXML($new), PHP_EOL;

Output with php:8.5-cli (PHP 8.5.12, libxml2 2.9.14):

Warning: DOMDocument::createElementNS(): unterminated entity reference  Co. KG) into a share (Projekt) is not allowed in /d.php on line 8
<o:reason xmlns:o="o:"/>
<o:reason xmlns:o="o:">Moving a storage (Muster GmbH &amp; Co. KG) into a share (Projekt) is not allowed</o:reason>

Output with php:8.5-cli-alpine (PHP 8.5.12, libxml2 2.13.9), no warning:

<o:reason xmlns:o="o:">Moving a storage (Muster GmbH  Co. KG) into a share (Projekt) is not allowed</o:reason>
<o:reason xmlns:o="o:">Moving a storage (Muster GmbH &amp; Co. KG) into a share (Projekt) is not allowed</o:reason>

End to end, verified on a Nextcloud 35.0.2 instance (PHP 8.5.12, libxml2 2.9.14):

  1. As bugtest-alice, create the folders Muster GmbH & Co. KG and Projekt and share both with bugtest-bob.
  2. As bugtest-bob, move Muster GmbH & Co. KG into Projekt via WebDAV:
curl -u bugtest-bob:PASS -X MOVE "https://HOST/remote.php/dav/files/bugtest-bob/Muster%20GmbH%20%26%20Co.%20KG" \
  -H "Destination: https://HOST/remote.php/dav/files/bugtest-bob/Projekt/Muster%20GmbH%20%26%20Co.%20KG"
  1. Check the response body and nextcloud.log.
Expected behavior

HTTP 403, with the full message in <o:reason>, and no PHP warning.

Actual behavior

HTTP 403. Note that <s:message> (built in Server.php) is escaped correctly, while <o:reason> in the same response is empty:

<?xml version="1.0" encoding="utf-8"?>
<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:o="http://owncloud.org/ns">
  <s:exception>OCA\DAV\Connector\Sabre\Exception\Forbidden</s:exception>
  <s:message>Moving a storage (Muster GmbH &amp; Co. KG) into a share (Projekt) is not allowed</s:message>
  <o:retry xmlns:o="o:">false</o:retry>
  <o:reason xmlns:o="o:"/>
</d:error>

The log gets this warning (from the reproduction run; first seen in production with the desktop client):

{
  "reqId": "xp5UETOUhmbX1eFxZSwi",
  "level": 2,
  "time": "2026-11-02T10:14:09+00:00",
  "remoteAddr": "fd00:beef:1337::1",
  "user": "bugtest-bob",
  "app": "PHP",
  "method": "MOVE",
  "url": "/remote.php/dav/files/bugtest-bob/Muster%20GmbH%20%26%20Co.%20KG",
  "scriptName": "/remote.php",
  "message": "DOMDocument::createElementNS(): unterminated entity reference  Co. KG) into a share (Projekt) is not allowed at /var/www/html/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php#46",
  "userAgent": "curl/8.14.1",
  "version": "35.0.2.2",
  "data": {
    "app": "PHP"
  }
}

message in <o:reason>, and no PHP warning.

Nextcloud Server version

35

Operating system

Debian/Ubuntu

PHP engine version

PHP 8.5

Web server

Apache (supported)

Database engine version

MariaDB

Is this bug present after an update or on a fresh install?

Fresh Nextcloud Server install

Are you using the Nextcloud Server Encryption module?

Encryption is Disabled

What user-backends are you using?
  • Default user-backend (database)
  • LDAP/ Active Directory
  • SSO - SAML
  • Other
Configuration report
{
    "system": {
        "htaccess.RewriteBase": "\/",
        "memcache.local": "\\OC\\Memcache\\APCu",
        "apps_paths": [
            {
                "path": "\/var\/www\/html\/apps",
                "url": "\/apps",
                "writable": false
            },
            {
                "path": "\/var\/www\/html\/custom_apps",
                "url": "\/custom_apps",
                "writable": true
            }
        ],
        "instanceid": "***REMOVED SENSITIVE VALUE***",
        "passwordsalt": "***REMOVED SENSITIVE VALUE***",
        "secret": "***REMOVED SENSITIVE VALUE***",
        "trusted_domains": [
            "example.com"
        ],
        "trusted_proxies": "***REMOVED SENSITIVE VALUE***",
        "datadirectory": "***REMOVED SENSITIVE VALUE***",
        "dbtype": "mysql",
        "version": "35.0.2.2",
        "overwrite.cli.url": "https:\/\/example.com",
        "overwriteprotocol": "https",
        "dbname": "***REMOVED SENSITIVE VALUE***",
        "dbhost": "***REMOVED SENSITIVE VALUE***",
        "dbport": "",
        "dbtableprefix": "oc_",
        "default_phone_region": "DE",
        "mysql.utf8mb4": true,
        "dbuser": "***REMOVED SENSITIVE VALUE***",
        "dbpassword": "***REMOVED SENSITIVE VALUE***",
        "installed": true,
        "mail_from_address": "***REMOVED SENSITIVE VALUE***",
        "mail_smtpmode": "smtp",
        "mail_sendmailmode": "smtp",
        "mail_domain": "***REMOVED SENSITIVE VALUE***",
        "mail_smtpsecure": "tls",
        "mail_smtphost": "***REMOVED SENSITIVE VALUE***",
        "mail_smtpauthtype": "LOGIN",
        "mail_smtpauth": 1,
        "mail_smtpname": "***REMOVED SENSITIVE VALUE***",
        "mail_smtppassword": "***REMOVED SENSITIVE VALUE***",
        "mail_smtpport": "587",
        "loglevel": 2,
        "maintenance": false,
        "memcache.distributed": "\\OC\\Memcache\\Redis",
        "memcache.locking": "\\OC\\Memcache\\Redis",
        "redis": {
            "host": "***REMOVED SENSITIVE VALUE***",
            "password": "***REMOVED SENSITIVE VALUE***",
            "port": 6379
        },
        "theme": "",
        "maintenance_window_start": 3,
        "upgrade.disable-web": true,
        "forbidden_filename_basenames": [
            "con",
            "prn",
            "aux",
            "nul",
            "com0",
            "com1",
            "com2",
            "com3",
            "com4",
            "com5",
            "com6",
            "com7",
            "com8",
            "com9",
            "com\u00b9",
            "com\u00b2",
            "com\u00b3",
            "lpt0",
            "lpt1",
            "lpt2",
            "lpt3",
            "lpt4",
            "lpt5",
            "lpt6",
            "lpt7",
            "lpt8",
            "lpt9",
            "lpt\u00b9",
            "lpt\u00b2",
            "lpt\u00b3"
        ],
        "forbidden_filename_characters": [
            "<",
            ">",
            ":",
            "\"",
            "|",
            "?",
            "*",
            "\\",
            "\/"
        ],
        "forbidden_filename_extensions": [
            " ",
            ".",
            ".filepart",
            ".part"
        ],
        "overwritehost": "example.com"
    }
}
List of activated Apps
Enabled:
  - activity: 8.0.0
  - admin_audit: 2.0.0
  - app_api: 35.0.0
  - appstore: 2.0.0
  - bruteforcesettings: 8.0.0
  - calendar: 6.6.3
  - circles: 35.0.0
  - cloud_federation_api: 2.0.0
  - comments: 2.0.0
  - contactsinteraction: 2.0.0
  - dashboard: 8.0.0
  - dav: 2.0.0
  - federatedfilesharing: 2.0.0
  - federation: 2.0.0
  - files: 3.0.0
  - files_downloadlimit: 5.3.0
  - files_fulltextsearch: 35.0.0
  - files_lock: 35.0.1
  - files_pdfviewer: 8.0.0
  - files_reminders: 2.0.0
  - files_sharing: 2.0.0
  - files_trashbin: 2.0.0
  - files_versions: 2.0.0
  - firstrunwizard: 8.0.0
  - fulltextsearch: 35.0.0
  - fulltextsearch_elasticsearch: 35.0.1
  - logreader: 8.0.0
  - lookup_server_connector: 2.0.0
  - nextcloud_announcements: 7.0.0
  - notifications: 8.0.1
  - oauth2: 2.0.0
  - office: 1.1.0
  - password_policy: 7.0.0
  - photos: 8.0.0
  - privacy: 7.0.0
  - profile: 2.0.0
  - provisioning_api: 2.0.0
  - recommendations: 8.0.0
  - related_resources: 6.0.0
  - serverinfo: 7.0.0
  - settings: 2.0.0
  - sharebymail: 2.0.0
  - sharing: 1.0.4
  - spreed: 25.0.6
  - support: 7.0.0
  - survey_client: 7.0.0
  - suspicious_login: 13.0.0
  - systemtags: 2.0.0
  - text: 9.0.1
  - theming: 3.0.0
  - twofactor_backupcodes: 2.0.0
  - twofactor_totp: 17.1.0
  - updatenotification: 2.0.0
  - user_status: 2.0.0
  - viewer: 8.0.0
  - weather_status: 2.0.0
  - webhook_listeners: 2.0.0
  - workflowengine: 3.0.0
Disabled:
  - cfg_share_links: 7.0.1 (installed 7.0.1)
  - encryption: 3.0.1
  - files_downloadactivity: 1.18.1 (installed 1.18.1)
  - files_external: 2.0.0
  - files_fulltextsearch_tesseract: 32.0.0 (installed 32.0.0)
  - twofactor_nextcloud_notification: 9.0.0
  - user_ldap: 2.0.0
  - whiteboard: 2.1.0 (installed 2.1.0)
Nextcloud Signing status
No errors have been found.
Nextcloud Logs
{
  "reqId": "xp5UETOUhmbX1eFxZSwi",
  "level": 2,
  "time": "2026-11-02T10:14:09+00:00",
  "remoteAddr": "fd00:beef:1337::1",
  "user": "bugtest-bob",
  "app": "PHP",
  "method": "MOVE",
  "url": "/remote.php/dav/files/bugtest-bob/Muster%20GmbH%20%26%20Co.%20KG",
  "scriptName": "/remote.php",
  "message": "DOMDocument::createElementNS(): unterminated entity reference  Co. KG) into a share (Projekt) is not allowed at /var/www/html/apps/dav/lib/Connector/Sabre/Exception/Forbidden.php#46",
  "userAgent": "curl/8.14.1",
  "version": "35.0.2.2",
  "data": {
    "app": "PHP"
  }
}
主要言語
PHP
スター
37.4k
フォーク
5.3k
平均マージ
2日 7時間
マージ済み PR(30日)
744

環境構築

Codespaces で開く

このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

nextcloud/server のほかの issue

nextcloud/server の issue をすべて見る

似ている issue

PHP の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。