SN-2026-024: GetObjectAttributes and Read-Only ZIP Archive Tag-Policy and GetObject Bypass
Status: Fixed on main (f746d0707, 296c9710b); scheduled for the next post-20261109 Server release
Identifier: SN-2026-024 (Security Advisory Ledger, #342)
Severity: High
Affected scope: SILO Server (RELEASE.2026-11-09T00-00-00Z and earlier) and inherited upstream MinIO releases since 2021-06-10 (ba5fb2365) and 2024-01-05 (9b8ba97f9)
Unaffected components: silo-console, mcli, silo-pkg, and kes
Standard GetObject and HeadObject handlers in SILO authenticate the caller first, read the addressed object version’s ObjectInfo, and then authorize s3:GetObject (or s3:GetObjectVersion) against objInfo.UserTags so IAM policies conditioned on s3:ExistingObjectTag/<key> take effect. Inherited from upstream MinIO, two read surfaces bypassed that post-metadata authorization step: GetObjectAttributes (GET /<bucket>/<key>?attributes) and the read-only .zip archive S3 extension (x-minio-extract: true on GET, HEAD, and ListObjectsV2). In addition, ListObjectsV2 archive member listing enumerated .zip contents using only bucket-level s3:ListBucket without checking s3:GetObject on the archive object itself, and reading an unindexed .zip archive persisted generated index metadata back onto the object during read verbs.
Summary
- What went wrong:
GetObjectAttributesskippeds3:ExistingObjectTagevaluation:getObjectAttributesHandler(cmd/object-handlers.go) rancheckRequestAuthType(which passesexistingObjectTags = "") beforeobjectAPI.GetObjectInfo, and never re-evaluateds3:GetObjectAttributes/s3:GetObject(or their*Version*counterparts) againstobjInfo.UserTags. Callers blocked byDenyons3:ExistingObjectTag/*received200 OKwith checksums, part counts, and sizes, while callers relying on tag-conditionedAllowstatements were falsely rejected (403 AccessDenied).- ZIP member
GET/HEADbypasseds3:ExistingObjectTag(x-minio-extract: true):getObjectInArchiveFileHandlerandheadObjectInArchiveFileHandler(cmd/s3-zip-handlers.go) likewise authorizeds3:GetObjectwith empty tags before reading the.zipobject’s metadata, allowing callers explicitly denied access to tagged.ziparchives (for example,classification=secret) to stream plaintext member files out of the archive by sendingGET /bucket/archive.zip/secret.txtwithx-minio-extract: true. - ZIP member
ListObjectsV2bypasseds3:GetObject:listObjectsV2InArchive(GET /bucket?list-type=2&prefix=archive.zip/withx-minio-extract: true) relied solely on the caller’s bucket-levels3:ListBucketcheck inlistObjectsV2Handler. A principal granteds3:ListBucket—even under an explicitDenyons3:GetObjectforarchive.zip—could read the.zipcentral directory from storage and enumerate every internal member filename, uncompressed byte size, and timestamp. - Read-path metadata mutation on unindexed
.zipobjects: When a.zipobject had not been uploaded with a prebuilt archive index (X-Amz-Meta-Snowball-Auto-Extract),readObjectZIPInfocalledPutObjectMetadataduringGET,HEAD, andListObjectsV2to persist the generated index onto the stored version—mutating object metadata from read-only HTTP verbs without write authorization.
- Upstream provenance: The
.zipextraction S3 extension was introduced in upstream MinIOba5fb2365(2021-06-10, minio/minio#12267);GetObjectAttributeswas introduced in9b8ba97f9(2024-01-05, minio/minio#18732). - Fix: Commits
f746d0707and296c9710bauthenticate upfront, authorize bothGetObjectAttributesactions and all three.zipread paths (GET,HEAD,ListObjectsV2) against the archive/object version’s storedUserTags, requires3:GetObjecton the.zipobject before listing its internal members, and keep read-time.zipindex construction strictly in memory viareadObjectZIPInfo(PutObjectMetadataruns only on uploads that explicitly request indexing).
1. Upstream Provenance and Root Cause
Why Pre-Lookup checkRequestAuthType Misses Object Tags
In cmd/auth-handler.go, checkRequestAuthType combines authenticateRequest and authorizeRequest (authorizeRequestWithExistingTags(..., "")) into a single call suitable for bucket-level or write-new-object handlers where no stored object tags exist yet. Handlers that read an existing object and support s3:ExistingObjectTag/<key>—such as GetObjectHandler and HeadObjectHandler—instead split the two steps:
- Call
authenticateRequestto verify the SigV4/presigned signature (or identify an anonymous caller) before touching storage. - Read
ObjectInfo(or attachopts.CheckPrecondFn) and callauthorizeRequestWithExistingTags(ctx, r, action, objInfo.UserTags)against the addressed version’s real tags.
Both ba5fb2365 (cmd/s3-zip-handlers.go) and 9b8ba97f9 (getObjectAttributesHandler in cmd/object-handlers.go) used checkRequestAuthType before GetObjectInfo and never re-ran policy evaluation once objInfo.UserTags was loaded:
- On
GetObjectAttributes, AWS S3 requires the caller to hold boths3:GetObjectAttributesands3:GetObject(ors3:GetObjectVersionAttributesands3:GetObjectVersionwhen?versionId=is supplied), evaluated against the target version’s stored tags. Evaluating both actions with""skipped any tag-conditionedDenyon either action and rejected any tag-conditionedAllow. - On
.zipmemberGETandHEAD(x-minio-extract: true), an attacker denieds3:GetObjectonarn:aws:s3:::vault/*whenevers3:ExistingObjectTag/confidential == "true"received403 AccessDeniedonGET /vault/bundle.zip, yet received200 OKand full member plaintext onGET /vault/bundle.zip/report.csvwithx-minio-extract: true.
Bucket-List Privilege Boundary vs. Archive Content Inspection
listObjectsV2Handler (cmd/bucket-listobjects-handlers.go) authorizes s3:ListBucket on the bucket before branching to listObjectsV2InArchive when r.Header.Get("x-minio-extract") == "true" and prefix contains .zip/. Unlike normal prefix listing—which reads only S3 object keys in the bucket namespace—listObjectsV2InArchive opens the .zip object’s payload bytes via getFilesListFromZIPObject (GetObjectNInfo) to parse its ZIP central directory. Treating internal archive payload parsing as covered by s3:ListBucket crossed the S3 data-access boundary: principals granted inventory-only s3:ListBucket (even with an explicit Deny on s3:GetObject) could inspect internal filenames and sizes inside confidential .zip objects.
2. Impact Scope
| Endpoint / Feature | Security & Functional Impact |
|---|---|
GetObjectAttributes (?attributes) |
Bypasses Deny rules conditioned on s3:ExistingObjectTag/* (disclosing ETags, checksums, object size, storage class, and multipart layout); falsely denies tag-conditioned Allow policies. |
ZIP GET / HEAD (x-minio-extract: true) |
Full plaintext read bypass of Deny rules conditioned on s3:ExistingObjectTag/* for any file stored inside a .zip object; false 403 on tag-conditioned Allow policies. |
ZIP ListObjectsV2 (x-minio-extract: true) |
Principals holding only s3:ListBucket can read the internal directory structure and member sizes of any .zip object in the bucket, bypassing Deny s3:GetObject and tag conditions. |
Unindexed .zip archive reads |
GET/HEAD/LIST previously invoked PutObjectMetadata to cache the generated ZIP index on disk, mutating stored metadata during read operations. |
3. Solution Design: Why It Is Necessary and Sufficient
Commits f746d0707 and 296c9710b align GetObjectAttributes and all three .zip archive read operations with SILO’s post-metadata tag authorization and read-only storage invariants (alongside 89028732a / #294 preserving 5xx storage errors on tag-conditioned GetObject/HeadObject reads):
GetObjectAttributes(cmd/object-handlers.go):- Selects
[s3:GetObjectAttributes, s3:GetObject](or[s3:GetObjectVersionAttributes, s3:GetObjectVersion]whenopts.VersionID != ""), runsauthenticateRequestupfront, readsobjInfo, and evaluates both required actions againstobjInfo.UserTagsviaauthorizeRequestWithExistingTags(orauthorizeRequestWithUnreadTagswhenGetObjectInforeturns an error). - Preserves standard S3 missing-object masking (
AccessDeniedwhen the caller lackss3:ListBucket,NoSuchKey/NoSuchVersionwhen permitted) while surfacing genuine retryable storage errors (503 SlowDownRead) to authorized callers instead of misreporting them as403.
- Selects
- ZIP
GET,HEAD, andListObjectsV2(cmd/s3-zip-handlers.go,cmd/bucket-listobjects-handlers.go):- Sets
ReqInfo.ObjectName = zipPath(the.zipobject’s own key, matching the ARN carrying the tags and permissions) and authenticates upfront onGET/HEAD. - Authorizes
s3:GetObjectagainstzipObjInfo.UserTagsboth on initialGetObjectInfoand insideopts.CheckPrecondFnwheneverGetObjectNInfoopens a byte range of the.ziparchive—closing any race if tags change between the metadata lookup and ranged payload read. - In
listObjectsV2InArchive, requiress3:GetObjectonzipPath(evaluated withzipObjInfo.UserTagsandauthorizeRequestWithUnreadTagson lookup errors), returningPrefixAccessDenied(403 AccessDenied) when the caller is not permitted to read the.zipobject.
- Sets
- Side-effect-free
readObjectZIPInfo(cmd/s3-zip-handlers.go):- Separates
readObjectZIPInfo(pure in-memory central-directory index builder used byGET,HEAD, andListObjectsV2) fromupdateObjectMetadataWithZipInfo(used exclusively by upload handlers when the uploader explicitly setsx-minio-extract: truewith write permission).
- Separates
Sufficiency and Necessity
- Necessary: Authorizing only on the initial
GetObjectInfoin the.ziphandlers would leave the subsequentGetObjectNInforange read unguarded against concurrent tag updates; re-checking insideopts.CheckPrecondFnbinds authorization to the exactObjectInfogeneration opened for reading. Likewise, separatingreadObjectZIPInfofromPutObjectMetadatais required so read-only callers neither mutatexl.metanor fail when reading archives in WORM-protected or read-only environments. - Sufficient: Every S3 entry point that reads
.zipmembers (getObjectInArchiveFileHandler,headObjectInArchiveFileHandler,listObjectsV2InArchive) andgetObjectAttributesHandlernow enforces post-lookupUserTagsevaluation, verified across allow/deny tag matrices, missing keys, and unreadable quorum states incmd/object-attributes-archive-tags_test.goandcmd/object-attributes-read-error_test.go.
4. Trade-Offs, Caveats, and Operational Costs
| Dimension | Engineering Trade-Off |
|---|---|
Repeated reads of unindexed .zip archives |
Previously, the first GET/HEAD/LIST with x-minio-extract: true on an ordinary .zip object wrote a compressed directory index back into xl.meta. Because read verbs are now strictly side-effect-free, unindexed .zip objects rebuild their directory index in memory from the ZIP central directory tail on each read. Workloads that repeatedly extract members from large .zip archives can still persist the index at upload time by passing x-minio-extract: true on PutObject / CompleteMultipartUpload. |
Archive member listing requires s3:GetObject |
Clients or scripts that previously listed inside prefix=archive.zip/ with x-minio-extract: true using a role that held only s3:ListBucket (without s3:GetObject on archive.zip) now receive 403 AccessDenied. Grant s3:GetObject on the archive prefix if archive content listing is intended. |
Upgrade and Mitigation Guidance
- Upgrade SILO Server to the post-
20261109release containingf746d0707and296c9710b. - Temporary mitigation prior to upgrade: On unpatched deployments that rely on
s3:ExistingObjectTag/*Denyrules to isolate sensitive objects (especially.ziparchives), block incoming client requests carrying thex-minio-extractHTTP header or?attributesquery parameter at the reverse proxy until the server upgrade is complete. - Review archive-listing IAM roles: If an internal application intentionally uses
ListObjectsV2withx-minio-extract: trueto browse.zipcontents, ensure its IAM policy includess3:GetObject(and satisfies any tag conditions) on those.zipobjects in addition tos3:ListBucket.
See the Security Advisory Ledger, Candidate Server Compatibility Notes, and SN-2026-022 (Active-Active Tagging Proxy Auth).