Skip to content

fix(dav): return 403 not 404 on denied write to a readable resource#61695

Open
ndo84bw wants to merge 1 commit into
nextcloud:masterfrom
ndo84bw:fix/dav-acl-forbidden-when-readable
Open

fix(dav): return 403 not 404 on denied write to a readable resource#61695
ndo84bw wants to merge 1 commit into
nextcloud:masterfrom
ndo84bw:fix/dav-acl-forbidden-when-readable

Conversation

@ndo84bw

@ndo84bw ndo84bw commented Jul 1, 2026

Copy link
Copy Markdown

Summary

Write access to a shared read-only calendar or shared read-only address book is answered by the server with 404 Not Found instead of 403 Forbidden. The user can see the resource (READ is allowed, GET returns 200 and it appears in PROPFIND), but a PUT or DELETE on it reports 404. Clients cannot derive a meaningful error from that.

The cause is DavAclPlugin::checkPrivileges: on an ACL denial, 404 is thrown unconditionally for every non-owner (protection against resource enumeration, originally owncloud/core#22578).

RFC 3744 (WebDAV Access Control Protocol) Section 3:

Servers MUST report a 403 "Forbidden" error if access is denied, except in the case where the privilege restricts the ability to know the resource exists, in which case 404 "Not Found" may be returned.

So returning 404 is fine, but only if the user cannot read the resource. Since the calendar is shared read-only, the user does have the right to see it and should get a 403.

The fix adds a read check before the 404: if the requesting user has READ on the resource, the real authorization error is returned - a 403 with the DAV:need-privileges body (RFC 3744 Section 7.1.1) - instead of the 404. Without read permission it stays 404. (An owner already received a plain 403 Forbidden from the pre-existing owner check and is unchanged.)

The change sits at a single central point through which all DAV ACL checks pass; it therefore applies uniformly to CalDAV and CardDAV and to all write operations (PUT, PROPPATCH, MKCOL/MKCALENDAR, bind/unbind = DELETE/MOVE/COPY).

Side note: the CalDAVTester suite apps/dav/tests/testsuits/caldavtest/tests/CalDAV/sharing-calendars.xml (test suite "Change to read-only calendar"), commented out since 2016, already expects 403 for a read-only PUT - the current behavior deviates from that. Reactivating this suite would be a reasonable follow-up step, but it is not part of this PR.

Tested

Unit
Three scenarios, each using a DataProvider against a Calendar AND an AddressBook node.

  • Non-owner with READ, without WRITE -> NeedPrivileges (403)
  • Non-owner without READ -> NotFound (404) with type-correct message
  • Owner without privilege -> Forbidden (403) << (Example is Birthday Calendar)

Live (on NC master)
Calendar and address book shared read-only by User1 to User2 via the web interface. User2 then attempted writing events into the read-only shared calendar and writing contacts into the read-only shared address book.

Via Thunderbird and via curl. Result:

Operation on read-only share missing privilege Before After
PUT create (CalDAV, If-None-Match:*) bind 404 403
PUT modify (CalDAV, If-Match) write-content 404 403
DELETE object (CalDAV) unbind 404 403
PUT create (CardDAV, read-only address book) bind 404 403
PROPPATCH displayname (write-properties IS allowed on the share) - 207 207 (unchanged)
PUT into calendar WITHOUT read permission (foreign admin calendar) - 404 404 (enumeration protection unchanged)

With Curl only:

Operation on read-only share Before After
MOVE object out of the read-only share 404 403
MKCALENDAR under a read-only, not writable calendar 404 403
MKCALENDAR in another user's calendar home without read access 404 404 (enumeration protection unchanged)
MKCOL 405 405 (Apache error, not our code)
Manual reproduction (curl)

Setup: User1 shares their "personal" calendar read-only with User2.
User2 mounts it at .../calendars/User2/personal_shared_by_User1/.

BASE=https://your-instance.example

# User2 CAN read the resource (its existence is not a secret):
curl -u User2:<pw> \
  "$BASE/remote.php/dav/calendars/User2/personal_shared_by_User1/evt.ics"
# -> 200 OK

# ...but writing it is denied. On master:
curl -u User2:<pw> -X PUT \
  "$BASE/remote.php/dav/calendars/User2/personal_shared_by_User1/evt.ics" \
  -H 'If-None-Match: *' -H 'Content-Type: text/calendar' --data-binary @evt.ics
# -> 404 Not Found  (Sabre\DAV\Exception\NotFound: "Calendar ... could not be found")

# With this change the same request returns:
# -> 403 Forbidden  (Sabre\DAVACL\Exception\NeedPrivileges; body carries <d:need-privileges>)

# The same 404 -> 403 flip holds for every write method routed through the ACL check,
# e.g. deleting, moving, or creating a collection in the read-only share:
curl -u User2:<pw> -X DELETE \
  "$BASE/remote.php/dav/calendars/User2/personal_shared_by_User1/evt.ics"
# master: 404  ->  with change: 403   (unbind)

curl -u User2:<pw> -X MOVE \
  "$BASE/remote.php/dav/calendars/User2/personal_shared_by_User1/evt.ics" \
  -H "Destination: $BASE/remote.php/dav/calendars/User2/personal/evt.ics"
# master: 404  ->  with change: 403   (unbind on the read-only source)

curl -u User2:<pw> -X MKCALENDAR \
  "$BASE/remote.php/dav/calendars/User2/personal_shared_by_User1/sub"
# master: 404  ->  with change: 403   (write on the read-only parent)

# Enumeration guard unchanged: a resource User2 may NOT read at all still returns 404,
# so its existence stays hidden. E.g. an unshared calendar of User1:
curl -u User2:<pw> -X PUT \
  "$BASE/remote.php/dav/calendars/User1/private/evt.ics" \
  -H 'If-None-Match: *' -H 'Content-Type: text/calendar' --data-binary @evt.ics
# -> 404 Not Found  (unchanged, before and after)

Checklist

AI (if applicable)

  • The content of this PR was partly or fully generated using AI
  • Helped analyze the code
  • Suggested code
  • Wrote the unit test
  • Built and executed the curl tests on patched master
  • Found the disabled test in sharing-calendars.xml
Assisted-by: ClaudeCode:claude-opus-4-8[1m]
Signed-off-by: Nico Donath <ndo84bw@gmx.de>
@ndo84bw ndo84bw requested a review from a team as a code owner July 1, 2026 14:48
@ndo84bw ndo84bw requested review from Altahrim, ArtificialOwl, leftybournes and nfebe and removed request for a team July 1, 2026 14:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant