Skip to content

[Bug]: Public file-drop into a group folder records wrong permissions #61012

Description

@brueckner

⚠️ This issue respects the following points: ⚠️

Bug description

Uploading a file through an "upload only" / file-request public share link that points inside a group folder stores the file with a broken permission set. Afterwards the team members who have access to the group folder can no longer view, edit or delete the dropped file.

This worked correctly in 32.0.9 and broke in 32.0.10.

Root cause: the public WebDAV endpoint apps/dav/appinfo/v2/publicremote.php (and the legacy apps/dav/appinfo/v1/publicwebdav.php) changed its sharePermissions storage wrapper from a whole-storage OC\Files\Storage\Wrapper\PermissionsMask to a OC\Files\Storage\Wrapper\DirPermissionsMask hard-coded to 'path' => 'files':

return new DirPermissionsMask([
    'storage' => $storage,
    'mask'    => $mask,
    'path'    => 'files',
]);

DirPermissionsMask::checkPath() only applies the mask to storage-internal paths equal to or under files/. That assumption is only valid for a user home storage (where shared content lives under files/, next to siblings like files_versions/, files_trashbin/, uploads/). A group folder is mounted as a Jail rooted at the group folder's own files area, so its internal storage paths do not carry a files/ prefix. checkPath() therefore never matches, the share mask is bypassed, and isCreatable()/isUpdatable()/isDeletable()/getPermissions() fall through to the unmasked underlying storage. As a result files dropped through an upload-only public link inside a group folder are recorded with the wrong permission set. 32.0.9 used the whole-storage PermissionsMask, which masks consistently regardless of storage layout, so the bug did not occur.

The files/-only masking was introduced in #59511 (to keep versions/trashbin usable for public shares on home storages) and extended to the v2 endpoint in #59654 ("Fix permission issue when uploading a chunked file"), which shipped in 32.0.10. The intent is correct for home storages, but the hard-coded 'files' path is wrong for jailed storages such as group folders.

Suggested fix: choose the masking strategy by storage type — keep DirPermissionsMask(path => 'files') for IHomeStorage, and use the whole-storage PermissionsMask for everything else (group folders / jailed / external storages), in both the v1 and v2 endpoints.

Steps to reproduce

  1. Create a group folder and give a group write access to it.
  2. Create a public share link on a folder inside that group folder and set it to "File request" / upload only (create permission only).
  3. Open the public link as an anonymous user and upload a file.
  4. As a group member, open the group folder and try to view/edit/delete the uploaded file.

Expected behavior

The uploaded file is stored with the normal group-folder permissions, so group members can read, edit and delete it (same as in 32.0.9).

Nextcloud Server version

32

Operating system

Debian/Ubuntu

PHP engine version

None

Web server

Apache (supported)

Database engine version

MySQL

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

Fresh Nextcloud Server install

Are you using the Nextcloud Server Encryption module?

None

What user-backends are you using?

  • Default user-backend (database)
  • LDAP/ Active Directory
  • SSO - SAML
  • Other

Configuration report

List of activated Apps

Enabled:
  - activity: 5.0.0
  - announcementcenter: 7.4.0
  - app_api: 32.0.0
  - bruteforcesettings: 5.0.0
  - calendar: 6.4.2
  - circles: 32.0.0
  - cloud_federation_api: 1.16.0
  - comments: 1.22.0
  - contacts: 8.3.12
  - contactsinteraction: 1.13.1
  - dashboard: 7.12.0
  - dav: 1.34.2
  - end_to_end_encryption: 1.18.2
  - federatedfilesharing: 1.22.0
  - federation: 1.22.0
  - files: 2.4.0
  - files_downloadlimit: 5.0.0
  - files_pdfviewer: 5.0.0
  - files_reminders: 1.5.0
  - files_sharing: 1.24.1
  - files_trashbin: 1.22.0
  - files_versions: 1.25.0
  - firstrunwizard: 5.0.0
  - groupfolders: 20.1.13
  - impersonate: 3.0.1
  - logreader: 5.0.0
  - lookup_server_connector: 1.20.0
  - mail: 5.8.2
  - notifications: 5.0.0
  - notify_push: 1.3.3
  - oauth2: 1.20.0
  - onlyoffice: 9.13.0
  - password_policy: 4.0.0
  - passwords: 2026.5.10
  - photos: 5.0.0
  - privacy: 4.0.0
  - profile: 1.1.0
  - provisioning_api: 1.22.0
  - recommendations: 5.0.0
  - related_resources: 3.0.0
  - richdocuments: 9.0.6
  - serverinfo: 4.0.0
  - settings: 1.15.1
  - sharebymail: 1.22.0
  - systemtags: 1.22.0
  - text: 6.0.2
  - theming: 2.7.0
  - theming_customcss: 1.20.0
  - twofactor_backupcodes: 1.21.0
  - twofactor_webauthn: 2.6.0
  - user_status: 1.12.0
  - viewer: 5.0.0
  - weather_status: 1.12.0
  - webhook_listeners: 1.3.0
  - workflowengine: 2.14.0
Disabled:
  - admin_audit: 1.22.0
  - encryption: 2.20.0
  - files_external: 1.24.1
  - nextcloud_announcements: 4.0.0 (installed 1.12.0)
  - support: 4.0.0 (installed 1.6.0)
  - survey_client: 4.0.0 (installed 1.11.0)
  - suspicious_login: 10.0.0
  - twofactor_nextcloud_notification: 6.0.0
  - twofactor_totp: 14.0.0
  - updatenotification: 1.22.0 (installed 1.18.0)
  - user_ldap: 1.23.0

Nextcloud Signing status

Nextcloud Logs

Additional info

The bug is present in v33.0.5, as well as in a fresh install of v30.0.10 and v30.0.11.

Regression introduced between v32.0.9 and v32.0.10 by commit 6b05ce6 (stable32 backport of #59654), a follow-up to #59511.

The legacy v1 endpoint apps/dav/appinfo/v1/publicwebdav.php carries the identical bug and should be fixed together with v2.


I think that the problem comes instead from getMetadata not being implemented for DirPermissionsMask. When a file is uploaded the file is scanned and its metadata is added to the cache. As DirPermissionsMask::getMetadata is not implemented it falls back to PermissionsMask::getMetadata, which restricts the permissions independently of the path. Although the metadata to be added to the cache contains masked permissions it also provides scan_permissions with the original ones, so before storing the permissions in the cache they are "reverted" to the original ones. Therefore, right after adding the file to the cache for the first time the permissions are the expected ones. But that is not the end of the story :-)

After storing the initial metadata if the request contains a modification time the cache is updated with it (which will be allowed by the DirPermissionsMask as the file is not in the path and thus the touch is delegated to the storage). This triggers another scan, which again gets the metadata with masked permissions even if the file is not in the path. However, as this is now a rescan there is cached data, but as the file is not in the path in this case the permissions are not masked, so the cached data contains a permissions entry with the right value (the same originally set) and no scan_permissions (which would have been set by the CachePermissionsMask if the path matched). Due to that, when the differences between the new data and the cached one are calculated the result is that permissions changed to the masked ones, but scan_permissions did not, so when added again to the cache the masked permissions are not "reverted" to the original ones like before and the masked permissions end stored in the cache. Due to all that the file does not have read, edit or delete permissions when accessed as a logged in user.

Note that getMetadata is not implemented either in DirMask in the guests app where DirPermissionsMask comes from, so if getMetadata should be added to DirPermissionsMask I guess it might need to be added to DirMask too.

Alternatively (or in addition?) to implementing getMetadata in DirPermissionsMask it might be necessary too to always set scan_permissions even if it did not change when calculating the differences between the new data and the cached one, as the cached value could be based already on scan_permissons rather than on permissions 🤔

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions