This is the security chronicle of the SILO community fork. It follows the incidents in the order they were investigated and fixed. Each CVE has its own article: the original threat model, the turns taken during review, the rejected alternatives, the final invariant, the evidence, and the compatibility cost all stay with that incident.
The articles below are ordered chronologically; incidents investigated on the same day are ordered by CVE number. Dependency-only CVEs remain in the relevant release notes instead of being inflated into application-level incident stories.
CVE-2026-32285: The jsonparser Advisory That Required No Patch
A security investigation that ended without a code change: the resolved dependency already contained the fix, and reachability analysis found no vulnerable path.
By Ruohang Feng |
Status: Closed without a code change GitHub issue:pgsty/minio#26
Security maintenance is not always a sequence of “find a vulnerability, then ship a patch.” The initial assessment of CVE-2026-32285 was that the repository might still carry a vulnerable jsonparser; replacing the dependency or maintaining another fork was even considered. Checking the resolved module version and actual reachability changed the conclusion: the tree already used v1.1.2, which contained the fix, and govulncheck found no reachable vulnerable symbol.
The right final action was not to manufacture an upgrade. It was to record the evidence and close the issue.
What was wrong with the initial assumption
The issue was first understood as “this dependency has no fixed version.” Acting on that premise without verification could have produced several changes that looked proactive but made the project worse:
changing the dependency graph for no security benefit;
introducing compatibility regressions in the name of a nonexistent fix;
adding another fork that would need long-term maintenance;
implying that previous SILO releases were demonstrably exposed when that had not been established.
Security work cannot be measured by whether it produces a diff. Leaving correct code unchanged is itself a security decision, and it needs evidence.
Investigation
The investigation narrowed the question through four layers of evidence:
Confirm the version actually selected in the current go.mod and go.sum graph.
Check the upstream release and establish that v1.1.2 already contained the relevant fix.
Run and inspect govulncheck; it reported no reachable vulnerable symbol.
Attribute the discrepancy in the issue to stale advisory or vulnerability-database information, not to a vulnerability still present in the source tree.
Four claims must remain separate: a version was once listed as affected, a package is imported, a vulnerable symbol is reachable in the program, and remote input can actually exploit that path. None of them proves the others.
Why there was no “just in case” upgrade
If the selected version already includes the fix, bumping to an arbitrary newer version does not make the system safer. It only expands the change surface and makes later regressions harder to attribute. That is especially risky in a large Go module graph.
The final decision was therefore to:
avoid committing a fictitious fix;
preserve the version and reachability evidence in the issue;
keep version gates and govulncheck in place to detect a future dependency rollback;
treat “no change required now” as a dated conclusion, not a permanent exemption.
Verification boundary
This incident established that the checkout examined on 2026-04-15 did not require a code change for CVE-2026-32285. It does not establish that every future branch, module graph, or release will remain unaffected. A dependency downgrade or a change in module selection requires the version and reachability checks to be repeated.
This article records the investigation and the basis for closure. The original govulncheck was not rerun while preparing this chronicle.
The principle this incident left behind
The objective of security maintenance is an accurate risk conclusion, not a patch for every issue. For a dependency CVE, ask in order: which version is actually selected, whether the vulnerable code enters the program, whether the symbol is reachable, and whether a deployed entry point makes it exploitable. Only when those answers require a source change should the investigation produce a diff.
CVE-2026-33322: OIDC JWT Algorithm Confusion
The OIDC verifier mixed a client secret with JWKS keys; restoring asymmetric, JWKS-only verification closed the algorithm-confusion path.
By Ruohang Feng |
Status: Released First containing release:RELEASE.2026-04-17T00-00-00Z Affected entry points:AssumeRoleWithWebIdentity, AssumeRoleWithClientGrants GitHub issue:pgsty/minio#22
The old implementation placed the OIDC client secret in the JWT verifier keyring while also accepting HMAC signing methods. An attacker who knew that client secret could therefore mint an HS-signed token and exchange it through STS for temporary credentials. The final fix restored asymmetric, JWKS-only verification. It deliberately broke HS256/384/512 compatibility instead of keeping an option that would reintroduce ambiguous trust semantics.
The vulnerability was not the absence of signature verification
At first glance, the old code did verify JWT signatures. The boundary that failed was more specific: which kind of key the verifier would accept, and whether the token header could select an algorithm with semantics that did not match that key’s intended role.
The attack chain required several conditions:
the attacker obtained the OIDC client secret;
the attacker constructed an HMAC-signed ID token;
the verifier treated the client secret as an HMAC signing key;
the token reached the WebIdentity or ClientGrants STS flow and was exchanged for temporary credentials.
Disclosure of a client secret is already serious, but it should not automatically confer the power to issue arbitrary user ID tokens. Combining those capabilities in one keyring created the algorithm-confusion vulnerability.
A compatibility path was implemented, then deliberately removed
During the fix, an allow_hmac-style compatibility path was implemented. It appeared reasonable: keep the secure default while letting users with a real requirement opt in. But retaining a shared secret in the general verifier keyring meant administrators would need to understand that the option expanded the entire STS trust boundary. Any future drift in the method allowlist could reopen the flaw.
The trade-off became clear:
Option
Benefit
Risk
Decision
Keep the secret keyring and restrict some algorithms
Small change; preserves HMAC IdPs
The keyring still mixes two trust semantics
Rejected
Add an allow_hmac option
Makes compatibility explicit
The option is difficult to reason about correctly and expands the test surface
Implemented, then reverted
JWKS-only verification
Clear boundary; refresh and retry use the same parser
HS users must migrate
Accepted
The most important decision was not what code was added, but that a completed compatibility implementation was removed.
Final invariants
The fix was concentrated in the OIDC JWT verification path and established four rules:
verifier keys come only from the identity provider’s JWKS;
the OIDC client secret never enters the JWT verification keyring;
HS256, HS384, and HS512 are always rejected;
the ordinary RS256 flow and JWKS refresh/retry use the same method allowlist.
The CVE was not used as a pretext for expanding JOSE support. PS256 and EdDSA remained out of scope.
Verification and release
The development record includes HS256 rejection, RS256 acceptance, JWKS refresh/retry regression tests, and focused go test ./internal/config/identity/openid. Temporary compatibility helpers, configuration, and tests were all removed from the final diff.
The public fix is f1f2239, released with SILO 2026-04-17. This article records the historical verification; those tests were not rerun while preparing the chronicle.
Compatibility cost
This is an explicit breaking change. Identity providers that still issue HS256/384/512 tokens must migrate to JWKS-backed RSA or ECDSA before upgrading SILO. The project chose a narrower trust model that is easier to explain and audit over preserving a configuration that happened to work before.
CVE-2026-33419: LDAP STS Enumeration and the Throttling Chain
From uniform authentication failures to corrected refunds, proxy attribution, and account lockout: LDAP STS hardening through two rounds of counter-fixes.
The core vulnerability was straightforward: LDAP STS returned different results for “user does not exist” and “password is wrong,” creating a username oracle. The first fix unified the external authentication failure and added limits by source IP and username. Continued review then showed that success refunds, spoofable source headers, reservation accounting, and the shared username bucket could turn the security control itself into a new attack surface.
The final June design removed the username bucket that enabled precise account lockout, retained only the source-IP bucket, and made proxy attribution an explicit deployment contract.
Initial threat model
The entry point is AssumeRoleWithLDAPIdentity. An attacker needs no existing MinIO account. Access to the LDAP STS endpoint is enough to compare the code, status, or message returned for an unknown user and a wrong password, enumerate valid usernames, and combine that knowledge with password spraying, guesses about organizational naming, or social engineering.
The fix could not simply disguise every error as “wrong password.” LDAP connection, lookup-bind, and directory-service failures still needed to appear as infrastructure errors, or operators would lose the ability to diagnose the service.
First round: uniform responses and a limiter
The initial fix on 2026-04-15 did three things:
unknown user and bad password returned the same external STS authentication error;
LDAP infrastructure failures still returned 500, with the real cause retained in the server log;
a new in-memory limiter initially created buckets for both source IP and normalized username.
This closed the content side channel and raised the cost of brute-force attempts, but the limiter state machine and source attribution exposed more problems.
Second round: success, attribution, and accounting
The follow-up changes on April 16 addressed three classes of defects:
Successful authentication must not consume the failure allowance; the reserve/commit/cancel/refund lifecycle had to be explicit.
The socket peer must be used by default; X-Forwarded-For, X-Real-IP, and Forwarded cannot be trusted merely because a request supplies them.
Refund and capacity need hard bounds so cancel logic cannot mint tokens.
A proxy must be placed on an explicit trusted allowlist before it can influence source attribution.
Third round: remove the username bucket
Adversarial review in June overturned the intuition that “source plus username must be stronger than source alone.” A shared username bucket could be exhausted continuously from arbitrary origins. With only a low request rate, an attacker could lock a targeted account before the legitimate user ever reached an LDAP bind.
The final fix therefore:
removed the per-username bucket;
peeled trusted hops from XFF right to left and selected the first untrusted address;
rejected trusted-proxy footguns such as 0.0.0.0/0 and ::/0;
stopped using Forwarded for security-sensitive bucketing;
allowed X-Real-IP only under a contract in which the proxy overwrites rather than forwards client input.
This turn in the review shows that a security control needs its own threat model. More dimensions of throttling do not automatically mean more security.
Rejected alternatives
Option
Why it was rejected
Perform a dummy bind for unknown users
Amplifies LDAP load and creates a second, error-prone authentication path after the content side channel is already closed
Bucket all IPv6 clients by /64
Legitimate users behind the same site or carrier prefix can throttle one another
Take the leftmost XFF value
Client-controlled and therefore spoofable
Fall back to the peer when XFF and X-Real-IP disagree
An attacker can create disagreement deliberately and collapse every user behind a proxy into one bucket
Fully support RFC 7239 Forwarded
Security-sensitive parsing complexity outweighs the practical benefit
Verification and release
The historical record covers limiter reserve/commit/cancel/refund behavior, concurrency, success and infrastructure failures, external equivalence of unknown-user and bad-password responses, and RemoteAddr, spoofed-header, trusted-proxy, multi-hop, and catch-all-CIDR cases. Focused package tests and builds were recorded as passing.
The LDAP security end-to-end test skips when _MINIO_LDAP_TEST_SERVER is absent, so an outer ok cannot be presented as proof of the full LDAP scenario.
Incorrect trusted-proxy configuration can still destroy source attribution.
A Forwarded-only deployment falls back to the peer bucket and loses granularity.
Rate limiting can reduce attempts from one source. The uniform external authentication response is what actually conceals whether a username exists.
CVE-2026-34204: Replication Metadata Injection
Ordinary PUT and COPY requests could forge internal replication state; the fix restores replication-only metadata solely inside authorized replication paths.
Ordinary PUT and COPY requests could smuggle X-Minio-Replication-* headers into internal X-Minio-Internal-* SSE metadata, creating objects whose replication state did not match the authorized path and could even make them unreadable. The final fix stopped accepting replication-only metadata by default, restored it only in a trusted flow authorized for ReplicateObjectAction, and sanitized CopyObject before any header consumer ran.
Threat model
An attacker needed only ordinary object-write permission, not internode credentials. The input came entirely from client-controlled X-Minio-Replication-* headers, but metadata extraction converted it into internal replication or SSE state.
Later read paths interpreted the object according to that false internal state. The result could be an unreadable object: an integrity and availability failure. Almost every production server accepting untrusted writes needed to be treated as affected.
The root problem was not the header name. It was that data from an untrusted source acquired internal semantics without passing replication authorization.
Reject the whole request, or sanitize precisely?
Rejecting an ordinary request whenever it contains a replication header is the most obvious fix. It would also turn a header clients were previously allowed to send and have ignored into a hard failure. The final design was more precise:
the default extraction path does not accept replication-only metadata;
ordinary PUT and COPY strip those fields first;
only a path authorized for ReplicateObjectAction restores them;
replica-status writes use the same trusted condition;
legitimate multipart and Snowball replication flows explicitly restore the SSE metadata they require.
That keeps the compatibility change inside internal semantics instead of expanding it to every client carrying an extraneous header.
Why CopyObject had to sanitize early
CopyObject headers are not used only for the final metadata map. They can be consumed earlier by precondition logic and SSE-C source handling. Removing them immediately before the object write is too late: earlier consumers have already been contaminated.
The final sanitization occurs before those consumers. “Untrusted replication headers never enter internal semantics” becomes one invariant instead of a convention every downstream function must remember to enforce.
Implementation and verification
The change covered handler utilities, object handlers, and multipart handlers, with tests at several layers:
trusted and untrusted metadata extraction at the helper layer;
malicious PUT and COPY cases at the handler layer;
CopyObject header sanitization;
red/green comparison between the vulnerable parent and the patched tree;
live-server before/after behavior showing that a malicious header no longer made an object unreadable;
continued operation of legitimate replication, multipart, and Snowball flows.
The public fix is fcb8f24. This article preserves the historical verification boundary; no live server was started again while preparing the chronicle.
Cost and residual risk
Internal replication headers supplied by ordinary clients are now ignored or stripped.
Replication-only metadata must be restored explicitly inside an authorized branch.
If a future replication entry point forgets to restore it, the result should be a functional regression rather than another untrusted write path.
The audit focused on replication headers; it does not establish that every X-Minio-Internal-* field has undergone the same trust review.
This incident leaves a simple review question: a field that looks “internal” is not necessarily trusted. Ask where it came from and which authorization decision allowed it to acquire internal meaning.
CVE-2026-39414: Oversized S3 Select Records and a SIMD Bypass
The first fix imposed a 1 MiB bound on CSV and JSON Lines; the second found that the SIMD fast path bypassed it completely.
The first fix in April reused the existing 1 MiB maxCharsPerRecord limit for both CSV and ordinary JSON Lines. This prevented unbounded buffering while waiting for a delimiter and returned the explicit OverMaxRecordSize error to clients. A June review then found that CPUs with SIMD support took a different simdjson fast path that bypassed the limit completely.
The final solution sent JSON Lines through the bounded reader on every CPU. The same review also corrected error mapping, parser errors, and the flush of completed records before a terminal error. SILO temporarily gave up the SIMD fast path in exchange for consistent security semantics.
Threat model
An attacker can submit or query an object containing an extremely long single record. The reader continues buffering until it sees a record delimiter, allowing memory and CPU denial of service. More subtly, the same input can select a different implementation according to the machine’s CPU features. Safe behavior on a test machine does not necessarily prove safe behavior in production.
Error semantics are part of the fix. If an oversized record appears only as a generic InternalError, clients and monitoring systems cannot distinguish an enforced security limit from a server failure.
First round: reuse the existing 1 MiB invariant
The first patch did not invent a new configuration knob. It applied the existing maxCharsPerRecord = 1 MiB rule:
the CSV splitter and line-delimited JSON rejected oversized records before buffering or parsing them further;
the earliest splitter error was preserved instead of being overwritten by a partial decode;
the error propagated as OverMaxRecordSize rather than collapsing into InternalError.
This was a deliberate compatibility contraction. Clients with lines or records larger than 1 MiB now had to split their input.
Second round: a hardware-dependent bypass
Following the call chain again in June exposed this path:
When simdjson.SupportedCPU() returned true, JSON Lines bypassed the bounded json.PReader. The third-party parser kept reading past a chunk boundary until it found a newline. A generic reader wrapper could not simultaneously preserve already completed records and guarantee a bound on the next record.
The final choice was not another wrapper. JSON Lines temporarily stopped using the SIMD path and always used the bounded PReader. If SIMD support returns, that implementation must enforce the same record limit itself and pass the same CPU-independent regression suite.
Stream semantics corrected in the same round
The review also fixed several adjacent behaviors:
use errors.As to pass through errors implementing SelectError, not just one concrete type;
have the JSON worker wrap parser failures as JSONParsingError;
flush completed records still waiting below the batch threshold before emitting a terminal error event;
preserve error priority in input order instead of letting a later oversized record overwrite an earlier parse error.
Those details determine whether a client sees the correct failure or a resource-limit fix that quietly broke the streaming protocol.
Deliberately left outside this CVE
The historical mismatch between CSV AllowQuotedRecordDelimiter and the outer physical-newline splitter.
Whether \r in CRLF counts toward the record length.
Restoring SIMD performance without an equivalent bound.
These questions may be real, but they require independent AWS-compatibility evidence or a more complex quote-aware splitter. They did not belong in a security patch based on guesses.
Verification and release
The historical record includes oversized JSON Lines, error-code preservation, and behavior tests that do not depend on the local machine’s SIMD capabilities. go test ./internal/s3select/... -count=1 and git diff --check were recorded as passing.
The initial public fix was c5765dc; the complete June correction is fd69c89. Those tests were not rerun while preparing this article.
Final trade-offs
JSON Lines performance may decrease; this incident did not produce a benchmark that quantifies it.
The 1 MiB per-record limit rejects oversized input accepted by previous releases.
Quoted, multiline CSV semantics still need separate work.
Any future CPU-specific fast path must share the slow path’s security tests.
The second fix leaves the central lesson: a security invariant must hold across hardware-dependent paths. A green test on one CPU does not prove that another execution engine is protected.
Snowball’s PutObjectExtractHandler omitted the streaming unsigned-trailer authentication case. A tar stream with a forged signature could enter untar() before authentication completed, and one request could fan out into many object writes. The final fix initialized the correct reader, handled the decoded length, and completed SigV4 verification before any tar byte reached the extractor.
Why the identifier changed
The official CVE had not been assigned when the fix was written, so the commit subject used the temporary identifier fake CVE-2026-40028. The final identifier is CVE-2026-40344. The historical commit was not rewritten; the advisory and this article use the official number.
From one missing authentication case to bulk object writes
The entry point was Snowball / PutObjectExtract auto-extraction. The request used unsigned-trailer streaming, an authentication type the old handler did not cover as ordinary PUT did.
The danger was larger than one incorrectly authorized request. Once the tar stream entered untar(), that request could create multiple attacker-chosen objects. An authentication omission was therefore amplified into a bulk-write problem.
Final invariant: the extractor sees zero bytes on failure
The key statement in the fix was:
If authentication ultimately fails, untar() must have seen zero bytes.
That rule excludes “extract first, then roll back if authentication fails.” Object writes travel through several paths, and proving a complete rollback is much harder than proving that input never crossed the boundary. Authentication had to close before data entered the extractor.
Implementation
The final change:
recognized authTypeStreamingUnsignedTrailer;
read X-Amz-Decoded-Content-Length;
used newUnsignedV4ChunkedReader();
performed complete SigV4 request verification before entering untar();
preserved valid signed Snowball requests and CRC32 trailer flows.
Verification
The historical commit and investigation record cover:
rejection of a forged-signature Snowball unsigned-trailer request;
rejection of anonymous Snowball writes to a non-public bucket;
successful extraction with a valid signature and trailing CRC32;
red/green comparison between the vulnerable parent and the patched tree;
containerized before/after smoke tests.
The public fix is b50ab58. The container tests were not rerun while preparing this article.
Compatibility and residual risk
Clients that relied on an unsigned-trailer Snowball combination whose signature was never really verified will fail after upgrading.
Authentication now closes before extraction, but tar paths, archive-size limits, and object-count limits remain separate security surfaces.
Snowball and ordinary unsigned-trailer requests now share a reader; future changes must regress both paths together.
The essence of the fix was not another if. It moved the authentication decision in front of the actual amplification boundary.
Query-string SigV4 credentials entered the unsigned-trailer stream without signature verification; the shared reader now closes that boundary once for every caller.
Query-string SigV4 credentials could enter a STREAMING-UNSIGNED-PAYLOAD-TRAILER data flow, while the old code verified the signature only when an Authorization header was present. A request carrying a valid access-key identifier could therefore complete a write without a correct signature.
The final fix moved presigned rejection and SigV4 verification into newUnsignedV4ChunkedReader(), making every caller consuming that stream share one authentication boundary.
Identifier note
The official CVE had not been assigned when the patch was written, so its commit subject used fake CVE-2026-40027. The final identifier is CVE-2026-41145. The historical commit remains unchanged; public material uses the official identifier.
Root cause: authentication was coupled to transport form
The affected entry points included PutObject and PutObjectPart. The request selected STREAMING-UNSIGNED-PAYLOAD-TRAILER, with its credentials and signature in the query string rather than the Authorization header.
The old handler used header presence to decide whether to verify a signature. The body reader still consumed the data normally, silently degrading query authentication into something close to an anonymous write. The attacker needed to know a valid access-key identifier, but did not need to produce a correct signature.
The problem was not failure to parse the query parameters. It was that authentication depended on how credentials were transported instead of the trust boundary at which the stream was consumed.
Why the patch did not live in each handler
Option
Risk
Decision
Add header/query checks separately to PutObject and PutObjectPart
Closes today’s entry points, but a new caller can omit the check again
Rejected
Invent a compatible presigned unsigned-trailer protocol
Greatly expands protocol and test surface without an existing support contract
Rejected
Reject and verify centrally in newUnsignedV4ChunkedReader()
Forces every consumer through the same boundary
Accepted
Anonymous unsigned-trailer requests were not prohibited wholesale. If bucket policy explicitly permits anonymous writes, they can still follow the anonymous authorization path. The forbidden state is the mixture of query credentials with no verification of those credentials.
Implementation and verification
The fix performs presigned rejection and SigV4 verification at the reader entry in cmd/streaming-v4-unsigned.go, while removing the gates in the PutObject and multipart handlers that depended on header presence.
New tests cover forged query PUT, multipart, mixed authentication, and anonymous policy. The historical record also includes a vulnerable-parent write that succeeded, failure on the patched tree, and live-server before/after smoke tests showing that header-authenticated and valid anonymous flows continued to work.
The public fix is fa7c579. The live exploit was not rerun while preparing this article.
Compatibility and residual risk
Presigned/query unsigned-trailer is now explicitly unsupported, an intentional breaking change.
Moving the fix into the reader significantly reduces the chance that a sibling handler omits the check again.
Other streaming authentication modes still need their own audits; this reader fix does not establish that every SigV4 streaming combination is safe.
The shape of this fix matters as much as its payload: when several handlers share an authenticated data stream, authentication belongs to the reader rather than to optional checks in each caller.
The msgpack body of /rmpl carried Bucket, Prefix, and Files. The old code joined those values into filesystem paths without a containment check. The initial fix implemented full preflight validation. Continued call-chain review then found that this API had had no production caller since 2024. The final solution changed from “retain and harden” to removing the route, handler, client, interface, and generated code.
Deleting roughly a thousand lines was a larger source diff than a local validation guard, but it left a smaller long-term attack surface.
Threat model
The vulnerable route was registered only in distributed erasure mode; single-node deployments were unaffected. An attacker needed an internode JWT derived from the root secret, control of a node, or the ability to intercept unencrypted traffic between nodes.
The dangerous fields were inside the msgpack body, not the URL or form data, so upper HTTP path middleware never saw them. xlStorage.ReadMultiple joined and read the resulting paths directly, allowing them to escape the drive root.
This was not an anonymous S3 vulnerability. It crossed the boundary from “cluster root or peer” to “any file readable by the node process.”
First design: retain the API and validate it completely
The initial patch in xlStorage.ReadMultiple:
rejected absolute paths, . and .. segments, backslashes, Windows drive prefixes, and NUL bytes;
checked final containment across drive, volume, prefix, and file;
preserved the historical contract for an empty Bucket and .minio.sys/multipart where possible;
returned an error before any read or streaming began.
That design could close the known traversal, but review quickly exposed an early-return gap.
MaxResults exposed the danger of validate-as-you-use
The first patch validated each Files item inside the read loop. For Files=[good, bad] with MaxResults=1, the function returned after reading the first item and never validated the second.
It did not read the second malicious file, but it violated the intended invariant that the entire msgpack request must be valid before execution. Validation was therefore moved into a complete preflight pass, with path-length checks also completed before streaming.
The general rule is useful beyond this endpoint: when a request can return early or stream a partial response, checking an element immediately before use is not equivalent to validating the whole request.
Final decision: delete the API
Further call-chain audit established that:
upstream removed the last production caller in September 2024;
multipart had moved to ReadParts;
the current tree had no in-tree production consumer;
upstream’s final remediation also deleted ReadMultiple.
The final change removed the route, handler, client wrapper, StorageAPI / xlStorage method, metric, datatype, and generated code. storageRESTVersion retained the existing compatibility strategy.
Option
Short-term change
Long-term maintenance surface
Decision
Validate in place
Smaller diff and preserved endpoint
Permanently retains an unused, privileged file-reading API
Abandoned
Delete the API
Removes more interface and generated code
Minimizes attack and maintenance surface
Accepted
Verification and release
The in-place validation phase ran focused tests for xlStorage, the storage-REST client, msgpack encode/decode, and path edge cases; adversarial review exposed the MaxResults flaw. The deletion phase checked the route, client, interface, generated surfaces, and absence of callers.
The public fix is 73ac524, released with SILO 2026-06-18. The post-deletion full suite was not rerun while preparing this article.
Compatibility and claim boundary
The external S3 API is unchanged.
Third-party implementations that privately called the internal /rmpl endpoint will stop working.
A mixed-version rolling upgrade may encounter a protocol mismatch, so cluster nodes should be kept on the same version during the upgrade.
Removing this endpoint proves only that ReadMultiple no longer exists; it does not establish that every internal node request carrying body paths has completed a containment audit.
This CVE reached the right final fix, but it also leaves an important distinction: closing one endpoint is not the same claim as closing an entire vulnerability class.