nextcloud / nextcloud/server

[Bug]: WebDAV PUT overwriting an existing dotfile returns 500 (data directory on CIFS/SMB, SSE enabled)

Open
#63,017 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

0. Needs triage 33-feedback bug
Dominant language
PHP
Stars
36.9k
Forks
5.2k
Avg merge
2d 3h
Merged PRs (30d)
713

Description

⚠️ This issue respects the following points: ⚠️
Bug description

A WebDAV PUT that overwrites an existing file whose basename begins with . always returns HTTP 500. Creating the same file succeeds. Overwriting a
non-dotfile succeeds. Overwriting the very same dotfile via occ files:put succeeds.

The practical effect is that dotfiles stop syncing silently and the server keeps its last-good copy indefinitely. For .gitignore this is damaging: a
stale, shorter .gitignore stays authoritative server-side, so other clients un-ignore whole build trees.

Note that the error message is misleading. It reports copied: -1 byte, expected filesize: N bytes, but -1 is the initialiser of $count in File::put() —
writeStream() throws before ever assigning it. It is not a byte count and does not indicate a short write.

Possibly the same as #21209 ("500 error when updating dotfiles", 2020), which reported the same trigger — leading dot, overwrite, CIFS-mounted data
directory with seal. It was closed in 2021 as a duplicate of #19317, which concerns trailing dots in directory names and is a different bug; no fix ever
landed. It may not be the same defect, though: #21209 ran without encryption, reported copied bytes: 0 rather than -1, and was diagnosed as Permission
denied in Local.php — a mechanism that does not reproduce here (see Additional info).

Steps to reproduce

No Nextcloud client required:

printf one > /tmp/x1
printf twotwotwo > /tmp/x2
B=https:///remote.php/dav/files/
A=":"

curl -su "$A" -T /tmp/x1 "$B/.curlprobe" -o /dev/null -w 'dot create: %{http_code}\n'
curl -su "$A" -T /tmp/x2 "$B/.curlprobe" -o /dev/null -w 'dot overwrite: %{http_code}\n'
curl -su "$A" -T /tmp/x1 "$B/curlprobe-plain" -o /dev/null -w 'plain create: %{http_code}\n'
curl -su "$A" -T /tmp/x2 "$B/curlprobe-plain" -o /dev/null -w 'plain overwrite: %{http_code}\n'

Observed:

dot create: 201 plain create: 201
dot overwrite: 500 <--- plain overwrite: 204

Only a leading dot in the basename predicts failure, and only on overwrite. All probes in one directory:

file create overwrite
probe-plain.txt 201 204 OK
noext (no extension at all) 201 204 OK
.probe-dotfile (byte-identical to probe-plain.txt) 201 500 FAIL
.trivial-name-test (8 bytes) 201 500 FAIL
.a.b (leading dot, non-empty pathinfo filename) 201 500 FAIL
.a.b failing rules out the pathinfo('.gitignore') empty-filename quirk; noext succeeding rules out anything extension-related.
Expected behavior

Overwriting a dotfile via WebDAV succeeds, as it does for every other filename, and as it does for the same file via occ files:put.

Nextcloud Server version

33

Operating system

Debian/Ubuntu

PHP engine version

PHP 8.3

Web server

Apache (supported)

Database engine version

PostgreSQL

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

None

Are you using the Nextcloud Server Encryption module?

Encryption is Enabled

What user-backends are you using?
  • Default user-backend (database)
  • LDAP/ Active Directory
  • SSO - SAML
  • Other
Configuration report
{
    "system": {
        "one-click-instance": true,
        "one-click-instance.user-limit": 100,
        "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
            }
        ],
        "check_data_directory_permissions": false,
        "memcache.distributed": "\\OC\\Memcache\\Redis",
        "memcache.locking": "\\OC\\Memcache\\Redis",
        "redis": {
            "host": "***REMOVED SENSITIVE VALUE***",
            "password": "***REMOVED SENSITIVE VALUE***",
            "port": 6379,
            "timeout": 3,
            "read_timeout": 10
        },
        "overwritehost": "trantor.justinscholz.de",
        "overwriteprotocol": "https",
        "passwordsalt": "***REMOVED SENSITIVE VALUE***",
        "secret": "***REMOVED SENSITIVE VALUE***",
        "trusted_domains": [
            "localhost",
            "trantor.justinscholz.de"
        ],
        "datadirectory": "***REMOVED SENSITIVE VALUE***",
        "dbtype": "pgsql",
        "version": "33.0.7.1",
        "overwrite.cli.url": "https:\/\/trantor.justinscholz.de\/",
        "dbname": "***REMOVED SENSITIVE VALUE***",
        "dbhost": "***REMOVED SENSITIVE VALUE***",
        "dbport": "",
        "dbtableprefix": "oc_",
        "dbuser": "***REMOVED SENSITIVE VALUE***",
        "dbpassword": "***REMOVED SENSITIVE VALUE***",
        "installed": true,
        "instanceid": "***REMOVED SENSITIVE VALUE***",
        "maintenance": false,
        "updatechecker": false,
        "loglevel": "2",
        "log_type": "file",
        "logfile": "\/var\/www\/html\/data\/nextcloud.log",
        "log_rotate_size": 10485760,
        "log.condition": {
            "apps": [
                "admin_audit"
            ]
        },
        "preview_max_x": 2048,
        "preview_max_y": 2048,
        "jpeg_quality": 60,
        "enable_previews": true,
        "upgrade.disable-web": true,
        "mail_smtpmode": "smtp",
        "trashbin_retention_obligation": "auto, 30",
        "versions_retention_obligation": "auto, 30",
        "activity_expire_days": 30,
        "simpleSignUpLink.shown": false,
        "share_folder": "\/Shared",
        "one-click-instance.link": "https:\/\/nextcloud.com\/all-in-one\/",
        "upgrade.cli-upgrade-link": "https:\/\/github.com\/nextcloud\/all-in-one\/discussions\/2726",
        "updatedirectory": "\/nc-updater",
        "maintenance_window_start": 100,
        "allow_local_remote_servers": true,
        "davstorage.request_timeout": 7200,
        "documentation_url.server_logs": "https:\/\/github.com\/nextcloud\/all-in-one\/discussions\/5425",
        "htaccess.RewriteBase": "\/",
        "dbpersistent": false,
        "auth.bruteforce.protection.enabled": true,
        "ratelimit.protection.enabled": true,
        "files_external_allow_create_new_local": false,
        "trusted_proxies": "***REMOVED SENSITIVE VALUE***",
        "preview_imaginary_key": "***REMOVED SENSITIVE VALUE***",
        "mail_sendmailmode": "smtp",
        "memories.db.triggers.fcu": true,
        "memories.exiftool": "\/var\/www\/html\/custom_apps\/memories\/bin-ext\/exiftool-amd64-musl",
        "memories.vod.path": "\/var\/www\/html\/custom_apps\/memories\/bin-ext\/go-vod-amd64",
        "memories.vod.ffmpeg": "\/usr\/bin\/ffmpeg",
        "memories.vod.ffprobe": "\/usr\/bin\/ffprobe",
        "memories.gis_type": 2,
        "mail_from_address": "***REMOVED SENSITIVE VALUE***",
        "mail_domain": "***REMOVED SENSITIVE VALUE***",
        "mail_smtpauth": true,
        "mail_smtphost": "***REMOVED SENSITIVE VALUE***",
        "mail_smtpport": "587",
        "mail_smtpname": "***REMOVED SENSITIVE VALUE***",
        "mail_smtppassword": "***REMOVED SENSITIVE VALUE***",
        "default_phone_region": "DE",
        "serverid": 445,
        "log_type_audit": "file",
        "logfile_audit": "\/var\/www\/html\/data\/audit.log",
        "update_channel": "stable",
        "eurooffice": {
            "editors_check_interval": 0,
            "jwt_secret": "2ec4989cbd196a2a8efa03b65fab8f950892afeb02806969",
            "jwt_header": "AuthorizationJwt"
        },
        "enabledPreviewProviders": {
            "24": "OCA\\Eurooffice\\Preview"
        },
        "filelocking.enabled": true,
        "DOMAIN": "trantor.justinscholz.de",
        "AIO_VERSION": "v13.4.1"
    }
}
List of activated Apps
Enabled:
  - activity: 6.0.0
  - admin_audit: 1.23.0
  - bruteforcesettings: 6.0.0
  - calendar: 6.5.2
  - circles: 33.0.0
  - cloud_federation_api: 1.17.0
  - collectives: 4.4.3
  - comments: 1.23.0
  - contacts: 8.7.5
  - contactsinteraction: 1.14.1
  - dashboard: 7.13.0
  - dav: 1.36.0
  - deck: 1.17.4
  - encryption: 2.21.0
  - eurooffice: 11.0.1
  - federatedfilesharing: 1.23.0
  - federation: 1.23.0
  - files: 2.5.0
  - files_downloadlimit: 5.1.0
  - files_external: 1.25.1
  - files_fulltextsearch: 33.0.0
  - files_pdfviewer: 6.0.0
  - files_reminders: 1.6.0
  - files_sharing: 1.25.2
  - files_trashbin: 1.23.0
  - files_versions: 1.26.0
  - firstrunwizard: 6.0.0
  - forms: 5.3.5
  - fulltextsearch: 33.0.0
  - fulltextsearch_elasticsearch: 33.0.0
  - logreader: 6.0.0
  - lookup_server_connector: 1.21.0
  - mail: 5.10.11
  - nextcloud-aio: 0.8.0
  - nextcloud_announcements: 5.0.0
  - notes: 6.0.1
  - notifications: 6.0.0
  - notify_push: 1.3.5
  - oauth2: 1.21.0
  - password_policy: 5.0.0
  - photos: 6.0.0
  - polls: 9.1.4
  - privacy: 5.0.0
  - profile: 1.2.0
  - provisioning_api: 1.23.0
  - recommendations: 6.0.0
  - related_resources: 4.0.0
  - serverinfo: 5.0.0
  - settings: 1.16.0
  - sharebymail: 1.23.0
  - spreed: 23.0.9
  - support: 5.0.0
  - survey_client: 5.0.0
  - suspicious_login: 11.0.0
  - systemtags: 1.23.0
  - tasks: 0.18.1
  - text: 7.0.1
  - theming: 2.8.0
  - twofactor_backupcodes: 1.22.0
  - twofactor_totp: 15.0.0
  - updatenotification: 1.23.0
  - user_status: 1.13.0
  - viewer: 6.0.0
  - weather_status: 1.13.0
  - webhook_listeners: 1.5.0
  - whiteboard: 1.5.9
  - workflowengine: 2.15.0
Disabled:
  - app_api: 33.0.0 (installed 5.0.2)
  - memories: 8.1.0 (installed 8.1.0)
  - twofactor_nextcloud_notification: 7.0.0
  - user_ldap: 1.24.0
Nextcloud Signing status
No errors have been found.
Nextcloud Logs
{"reqId":"...","level":3,"app":"webdav","method":"PUT",
   "url":"/remote.php/dav/files/<user>/<path>/.gitignore",
   "message":"Error while writing stream to storage: Failed to open files/<path>/.gitignore for writing",
   "version":"33.0.7.1",
   "exception":{
     "Exception":"OCP\\Files\\GenericFileException",
     "Message":"Failed to open files/<path>/.gitignore for writing",
     "File":"/var/www/html/lib/private/Files/Storage/Wrapper/Encryption.php",
     "Line":915,
     "Trace":[
       {"file":"/var/www/html/lib/private/Files/Storage/Wrapper/Wrapper.php","line":330,
        "function":"writeStream","class":"OC\\Files\\Storage\\Wrapper\\Encryption"},
       {"file":"/var/www/html/apps/dav/lib/Connector/Sabre/File.php","line":223,
        "function":"writeStream","class":"OC\\Files\\Storage\\Wrapper\\Wrapper"},
       {"file":"/var/www/html/3rdparty/sabre/dav/lib/DAV/Server.php","line":1137,
        "function":"put","class":"OCA\\DAV\\Connector\\Sabre\\File"}
     ]}}

  {"reqId":"...","level":3,"method":"PUT","message":"Uncaught exception",
   "exception":{
     "Exception":"Sabre\\DAV\\Exception",
     "Message":"Error while copying file to target location (copied: -1 byte, expected filesize: 1322 bytes)",
     "File":"/var/www/html/apps/dav/lib/Connector/Sabre/File.php",
     "Line":244}}
Additional info

Encryption::writeStream() throws because Encryption::fopen($path, 'w') returned false.

Ruled out, each tested directly:

  • Filesystem / SMB. Data directory is a CIFS/SMB3 mount (Hetzner Storage Box). Dotfile create and overwrite succeed on that mount as root and as
    www-data, including aged well past actimeo=1/closetimeo=1.
  • PHP itself. As www-data inside the container, php -r 'var_dump(fopen("/.d","w"));' returns a resource for a dotfile on that mount, with no
    error. So neither PHP nor the kernel is refusing the open.
  • Filename policy. blacklisted_files, forbidden_filenames, forbidden_filename_basenames, forbidden_filename_characters, forbidden_filename_extensions are
    all unset.
  • File corruption / keys. occ files:get /dev/null decrypts the existing file cleanly.
  • occ files:put overwrites the same dotfile successfully, through the same wrapper onto the same storage.
  • Content, size, specific name. See the probe table — byte-identical content succeeds under a non-dot name, and an 8-byte dotfile fails.
  • Source review. Encryption::fopen() on stable33 contains no basename, pathinfo, dot or hidden-file logic; its only explicit return false passes through
    a non-resource from the wrapped storage.

So raw PHP fopen($path,'w') on the exact path works, while Encryption::fopen() on the same path returns false. The gap is inside the storage-wrapper
chain. One link I could not test from outside is \OC\Files\Stream\Encryption::wrap(), which fopen() returns directly and which can itself return false.

Possibly relevant: in the failing trace, writeStream() receives the live path, not a part file — no

/.ocTransferId….part. So
needsPartFile() appears to be false here and DAV truncates the real encrypted file in place.

Mount options for the data directory:

cifs (rw,relatime,vers=3.1.1,cache=strict,seal,hard,nounix,serverino,mapposix,
forceuid,uid=33,forcegid,gid=33,file_mode=0755,dir_mode=0755,
actimeo=1,closetimeo=1,rsize=4194304,wsize=4194304)

Workaround: delete the file and let it be re-created; do not retry the overwrite. Renaming is not a workaround — the desktop client detects the rename
and issues a MOVE, which preserves the stale server-side content.

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 at apps/dav/lib/Connector/Sabre/File.php, especially put(), and follow the writeStream() path through lib/private/Files/Storage/Wrapper/Encryption.php, where Encryption::fopen() returns false. Reproduce the WebDAV create-and-overwrite sequence with a leading-dot filename, then compare it with occ files:put and a non-dotfile overwrite. Done means WebDAV overwrites dotfiles successfully without the misleading 500 response.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.