From 6ed29f8c0471ee247ef3596dcdc2ea338a1c3969 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 21 Oct 2026 13:00:18 -0700 Subject: [PATCH] s3tables: authorize CreateNamespace before revealing a missing bucket A bucket that is absent or not a table bucket used to answer 404 before the permission check ran, while a denied existing bucket answered 403. An unauthorized caller now gets the same denial unless it may learn the name is absent (admin, trusted handler, identity grant). --- weed/s3api/s3tables/handler.go | 14 ++++++++++++++ weed/s3api/s3tables/handler_namespace.go | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/weed/s3api/s3tables/handler.go b/weed/s3api/s3tables/handler.go index 608771a037..bd9ed2abe4 100644 --- a/weed/s3api/s3tables/handler.go +++ b/weed/s3api/s3tables/handler.go @@ -459,6 +459,20 @@ func isAuthError(err error) bool { return errors.As(err, &authErr) || errors.Is(err, ErrAccessDenied) } +// mayLearnAbsence reports whether the caller may be shown that an operation's +// target is absent rather than the operation's denial: the admin, a trusted +// handler, or a grant the request itself carries (an identity-level action +// grant, or the open default-allow context). +func (h *S3TablesHandler) mayLearnAbsence(r *http.Request, operation, bucketName string) bool { + if h.getAccountID(r) == s3_constants.AccountAdminId || h.defaultAllowFor(r) { + return true + } + return hasIdentityPermission(operation, &PolicyContext{ + TableBucketName: bucketName, + IdentityActions: getIdentityActions(r), + }) +} + func (h *S3TablesHandler) readTags(ctx context.Context, client filer_pb.SeaweedFilerClient, path string) (map[string]string, error) { data, err := h.getExtendedAttribute(ctx, client, path, ExtendedKeyTags) if err != nil { diff --git a/weed/s3api/s3tables/handler_namespace.go b/weed/s3api/s3tables/handler_namespace.go index f71503fcc5..d704c9cd98 100644 --- a/weed/s3api/s3tables/handler_namespace.go +++ b/weed/s3api/s3tables/handler_namespace.go @@ -140,6 +140,12 @@ func (h *S3TablesHandler) handleCreateNamespace(w http.ResponseWriter, r *http.R return ErrAccessDenied } if errors.Is(err, filer_pb.ErrNotFound) { + // An absent name and a denied one must read alike; the caller is + // told the bucket is missing only when it may learn that. + if !h.mayLearnAbsence(r, "CreateNamespace", bucketName) { + h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to create namespace in this bucket") + return ErrAccessDenied + } h.writeError(w, http.StatusNotFound, ErrCodeNoSuchBucket, fmt.Sprintf("table bucket %s not found", bucketName)) } else { h.writeError(w, http.StatusInternalServerError, ErrCodeInternalError, fmt.Sprintf("failed to check table bucket: %v", err))