fix: keep streaming trailers visible after replication headers are stripped

A request that does not earn replication trust continues with a clone whose
internal replication headers are removed. r.Clone copies the Trailer map, but
the streaming body reader created from the original request fills the
original map, so a trailing checksum was never seen by the hash reader and
PutObject and UploadPart with STREAMING-UNSIGNED-PAYLOAD-TRAILER failed with
XAmzContentChecksumMismatch whenever an untrusted X-Minio-Source-* header was
present. Share the trailer map with the clone, as the Snowball path already
does for its per-entry requests, and cover both handlers with a test.

The marker evaluation that was copied into six handlers now lives in
evaluateReplicationTrust so the rule (a declared replica without the
replication permission is rejected; trust needs the exact marker plus the
permission) is defined once.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PvgysXDmhPBBimCReYtA8q
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-09-02 14:06:56 +08:00
parent 6586fbfd0d
commit 76195f1c68
4 changed files with 95 additions and 59 deletions
+22
View File
@@ -70,6 +70,25 @@ func replicationPermissionAllowed(ctx context.Context, r *http.Request, bucket,
return authorizeRequest(ctx, r, action) == ErrNone
}
// evaluateReplicationTrust decides whether a request may carry replication
// semantics for the given action. A request that declares itself a replica
// without holding the replication permission is rejected. trusted reports that
// the exact marker came from a permitted principal; replica additionally
// requires the request to declare REPLICA status.
func evaluateReplicationTrust(ctx context.Context, r *http.Request, bucket, object string, action policy.Action) (trusted, replica bool, s3Err APIErrorCode) {
rawReplica := hasReplicaStatus(r.Header)
markerExact := hasReplicationMarker(r.Header)
permitted := false
if rawReplica || markerExact {
permitted = replicationPermissionAllowed(ctx, r, bucket, object, action)
}
if rawReplica && !permitted {
return false, false, ErrAccessDenied
}
trusted = markerExact && permitted
return trusted, trusted && rawReplica, ErrNone
}
// replicationRequestHeaders are internal request controls. They are removed
// only after signature verification when a request has not earned replication
// trust. Public S3/SSE/checksum headers, proxy loop guards, and replication
@@ -110,6 +129,9 @@ func hasReplicationRequestHeaders(h http.Header) bool {
func cloneRequestWithoutReplicationHeaders(ctx context.Context, r *http.Request) *http.Request {
clone := r.Clone(ctx)
stripReplicationRequestHeaders(clone.Header)
// A streaming body reader built from the original request fills r.Trailer
// as the body is consumed; the checksum reader must observe that same map.
clone.Trailer = r.Trailer
return clone
}