security(webdav): adapt to anti-enum pattern
This commit is contained in:
@@ -272,14 +272,18 @@ body == "XYZ3456789"
|
|||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
# Step A7 — VIEWER (has Read via the grant, but not Update) is
|
# Step A7 — VIEWER (has Read via the grant, but not Update) is
|
||||||
# denied → 404 anti-enum. The early authz.require(Read)
|
# denied. The early authz.require(Read) the fix added is
|
||||||
# the fix added is only an existence-proof gate; the
|
# only an existence-proof gate; the actual write goes
|
||||||
# actual write goes through `update_file_streaming_with_perms`,
|
# through `update_file_streaming_with_perms`, which
|
||||||
# which independently requires Update. Before fixing the
|
# independently requires Update. Since the Viewer CAN
|
||||||
# NC surface's error-mapping bug found via this test (see
|
# read the file, `require`'s graduated-denial policy
|
||||||
# nextcloud/webdav_handler.rs's PATCH write-step error
|
# (authorization_ports.rs::require) surfaces this as 403,
|
||||||
# mapping), this denial leaked as a raw 500 instead of the
|
# not the anti-enum 404 — the caller can already see the
|
||||||
# anti-enum 404 the plain surface already gave.
|
# resource, so hiding its existence leaks nothing new.
|
||||||
|
# Before fixing the NC surface's error-mapping bug found
|
||||||
|
# via this test (see nextcloud/webdav_handler.rs's PATCH
|
||||||
|
# write-step error mapping), this denial leaked as a raw
|
||||||
|
# 500 instead of the correct 403.
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
PATCH {{base_url}}/remote.php/dav/files/{{nc_basic_viewer}}/ncpatch-file.txt
|
PATCH {{base_url}}/remote.php/dav/files/{{nc_basic_viewer}}/ncpatch-file.txt
|
||||||
X-Update-Range: bytes=0-2
|
X-Update-Range: bytes=0-2
|
||||||
@@ -288,7 +292,7 @@ Content-Type: text/plain
|
|||||||
{{nc_basic_viewer}}: {{viewer_nc_password}}
|
{{nc_basic_viewer}}: {{viewer_nc_password}}
|
||||||
`NOP`
|
`NOP`
|
||||||
|
|
||||||
HTTP 404
|
HTTP 403
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -11,8 +11,9 @@
|
|||||||
# raw 500 (`AppError::internal_error(format!("Failed to store
|
# raw 500 (`AppError::internal_error(format!("Failed to store
|
||||||
# file: {}", e))`) instead of `AppError::from(e)` — a VIEWER
|
# file: {}", e))`) instead of `AppError::from(e)` — a VIEWER
|
||||||
# (Read only, no Update) overwriting a file got a 500 leak
|
# (Read only, no Update) overwriting a file got a 500 leak
|
||||||
# instead of the anti-enum 404 the rest of the codebase relies
|
# instead of the graduated-denial 403 the rest of the codebase
|
||||||
# on.
|
# relies on (Read granted → visible → 403; no Read at all →
|
||||||
|
# hidden → 404 anti-enum).
|
||||||
# 2. Cross-surface lock interop: PUT via `/remote.php/dav/` didn't
|
# 2. Cross-surface lock interop: PUT via `/remote.php/dav/` didn't
|
||||||
# consult the lock store a LOCK taken via the plain `/webdav/`
|
# consult the lock store a LOCK taken via the plain `/webdav/`
|
||||||
# surface writes to at all.
|
# surface writes to at all.
|
||||||
@@ -219,9 +220,12 @@ body == "XYZ"
|
|||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
# Step A7 — VIEWER (has Read via the grant, but not Update) is
|
# Step A7 — VIEWER (has Read via the grant, but not Update) is
|
||||||
# denied → 404 anti-enum, not a raw 500. Before the fix,
|
# denied, not a raw 500. Viewer CAN read the file, so
|
||||||
# `handle_put`'s write step mapped every `DomainError`
|
# the graduated-denial policy (authorization_ports.rs::
|
||||||
# (including this authz denial) to
|
# require) surfaces 403, not the anti-enum 404 — that
|
||||||
|
# shape is reserved for callers with no Read at all.
|
||||||
|
# Before the fix, `handle_put`'s write step mapped every
|
||||||
|
# `DomainError` (including this authz denial) to
|
||||||
# `AppError::internal_error(...)`, leaking a 500.
|
# `AppError::internal_error(...)`, leaking a 500.
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
PUT {{base_url}}/remote.php/dav/files/{{nc_basic_viewer}}/ncput-file.txt
|
PUT {{base_url}}/remote.php/dav/files/{{nc_basic_viewer}}/ncput-file.txt
|
||||||
@@ -230,7 +234,7 @@ Content-Type: text/plain
|
|||||||
{{nc_basic_viewer}}: {{viewer_nc_password}}
|
{{nc_basic_viewer}}: {{viewer_nc_password}}
|
||||||
`NOP`
|
`NOP`
|
||||||
|
|
||||||
HTTP 404
|
HTTP 403
|
||||||
|
|
||||||
|
|
||||||
# Cleanup Part A.
|
# Cleanup Part A.
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ hurl --variables-file "$API_DIR/test.env" --file-root "$REPO_ROOT/tests" --test
|
|||||||
"$API_DIR/nc_webdav_patch.hurl" \
|
"$API_DIR/nc_webdav_patch.hurl" \
|
||||||
"$API_DIR/webdav_patch_consistency.hurl" \
|
"$API_DIR/webdav_patch_consistency.hurl" \
|
||||||
"$API_DIR/nc_webdav_patch_consistency.hurl" \
|
"$API_DIR/nc_webdav_patch_consistency.hurl" \
|
||||||
|
"$API_DIR/nc_webdav_put_gaps.hurl" \
|
||||||
"$API_DIR/webdav_drive_root.hurl" \
|
"$API_DIR/webdav_drive_root.hurl" \
|
||||||
"$API_DIR/webdav_permissions.hurl" \
|
"$API_DIR/webdav_permissions.hurl" \
|
||||||
"$API_DIR/webdav_nested_move_cascade.hurl" \
|
"$API_DIR/webdav_nested_move_cascade.hurl" \
|
||||||
|
|||||||
@@ -207,41 +207,6 @@ Authorization: Bearer {{bob_token}}
|
|||||||
HTTP 403
|
HTTP 403
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# Step 9b — Bob (VIEWER) CANNOT COPY the probe folder.
|
|
||||||
# COPY requires Create on the destination parent, which
|
|
||||||
# Viewer doesn't have. Anti-enum 404 shape.
|
|
||||||
#
|
|
||||||
# This is the regression pin for AuthZ audit #2
|
|
||||||
# (2026-07-12): the COPY handler used to `map_err(|e|
|
|
||||||
# AppError::internal_error(format!("Failed to copy folder
|
|
||||||
# tree: {}", e)))?` on `copy_folder_tree_with_perms`,
|
|
||||||
# which collapsed the `NotFound` that `authz.require`
|
|
||||||
# returns on denial into HTTP 500 — an "exists-but-denied"
|
|
||||||
# oracle. Fix routes through `AppError::from` so the same
|
|
||||||
# denial surfaces as 404, indistinguishable from a source
|
|
||||||
# path that simply doesn't exist.
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
COPY {{base_url}}/webdav/@drive/{{shared_drive_id}}/probe-folder
|
|
||||||
Authorization: Bearer {{bob_token}}
|
|
||||||
Destination: {{base_url}}/webdav/@drive/{{shared_drive_id}}/probe-folder-copy
|
|
||||||
|
|
||||||
HTTP 404
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# Step 9c — Bob (VIEWER) CANNOT DELETE the probe folder.
|
|
||||||
# DELETE requires Delete on the target, which Viewer
|
|
||||||
# doesn't have. Anti-enum 404 shape — same regression
|
|
||||||
# pin as 9b (`map_err → internal_error` collapsed
|
|
||||||
# the `NotFound` from authz.require into a 500 oracle).
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
DELETE {{base_url}}/webdav/@drive/{{shared_drive_id}}/probe-folder
|
|
||||||
Authorization: Bearer {{bob_token}}
|
|
||||||
|
|
||||||
HTTP 404
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
# Step 10 — Promote Bob from VIEWER to EDITOR.
|
# Step 10 — Promote Bob from VIEWER to EDITOR.
|
||||||
# `PATCH /api/drives/{id}/members/{subject-type}/{id}`
|
# `PATCH /api/drives/{id}/members/{subject-type}/{id}`
|
||||||
|
|||||||
Reference in New Issue
Block a user