Merge pull request #85 from pgsty/codex/server-prerelease-foundation

fix: restore pre-release server corrections
This commit is contained in:
Feng Ruohang
2026-08-29 18:40:26 +08:00
committed by GitHub
16 changed files with 351 additions and 36 deletions
+19
View File
@@ -10,6 +10,25 @@ Security fixes are tracked on the active development branch and summarized in
[docs/security/advisories.md](docs/security/advisories.md). Only the current
Silo release line is supported unless an advisory says otherwise.
## Inherited Fix Evidence
The canonical ledger also records security fixes inherited from upstream when
they are part of the Silo release baseline. Source and fork commits are linked
separately even when the fork preserves the original commit object and SHA.
- [CVE-2025-62506](https://github.com/advisories/GHSA-jjjj-jwhf-8rgr):
upstream [PR #21642](https://github.com/minio/minio/pull/21642) merged as
[`minio/minio@c1a49490`](https://github.com/minio/minio/commit/c1a49490c78e9c3ebcad86ba0662319138ace190),
inherited unchanged as
[`pgsty/silo@c1a49490`](https://github.com/pgsty/silo/commit/c1a49490c78e9c3ebcad86ba0662319138ace190),
and is present in every Silo community release beginning with
[`RELEASE.2025-12-03T12-00-00Z`](https://github.com/pgsty/silo/releases/tag/RELEASE.2025-12-03T12-00-00Z).
The inherited [service-account](https://github.com/pgsty/silo/blob/c1a49490c78e9c3ebcad86ba0662319138ace190/cmd/admin-handlers-users_test.go#L211-L212)
and [STS](https://github.com/pgsty/silo/blob/c1a49490c78e9c3ebcad86ba0662319138ace190/cmd/sts-handlers_test.go#L45-L46)
regression groups remain part of `go test ./cmd`; see the
[canonical ledger](docs/security/advisories.md#inherited-upstream-advisory-baseline)
for the operator-facing record.
## Reporting a Vulnerability
For vulnerabilities in this fork:
+12 -5
View File
@@ -355,18 +355,25 @@ func (a adminAPIHandlers) ListGroups(w http.ResponseWriter, r *http.Request) {
}
// SetGroupStatus - PUT /minio/admin/v3/set-group-status?group=mygroup1&status=enabled
func setGroupStatusAdminAction(status string) policy.AdminAction {
if madmin.GroupStatus(status) == madmin.GroupDisabled {
return policy.DisableGroupAdminAction
}
return policy.EnableGroupAdminAction
}
func (a adminAPIHandlers) SetGroupStatus(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
objectAPI, _ := validateAdminReq(ctx, w, r, policy.EnableGroupAdminAction)
if objectAPI == nil {
return
}
vars := mux.Vars(r)
group := vars["group"]
status := vars["status"]
objectAPI, _ := validateAdminReq(ctx, w, r, setGroupStatusAdminAction(status))
if objectAPI == nil {
return
}
var (
err error
updatedAt time.Time
+121
View File
@@ -68,6 +68,26 @@ func TestSetUserStatusAdminAction(t *testing.T) {
}
}
func TestSetGroupStatusAdminAction(t *testing.T) {
tests := []struct {
name string
status string
want policy.AdminAction
}{
{name: "enable", status: string(madmin.GroupEnabled), want: policy.EnableGroupAdminAction},
{name: "disable", status: string(madmin.GroupDisabled), want: policy.DisableGroupAdminAction},
{name: "invalid preserves authenticated default", status: "invalid", want: policy.EnableGroupAdminAction},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := setGroupStatusAdminAction(tt.status); got != tt.want {
t.Fatalf("setGroupStatusAdminAction(%q) = %q, want %q", tt.status, got, tt.want)
}
})
}
}
// API suite container for IAM
type TestSuiteIAM struct {
TestSuiteCommon
@@ -224,6 +244,7 @@ func TestIAMInternalIDPServerSuite(t *testing.T) {
suite.SetUpSuite(c)
suite.TestUserCreate(c)
suite.TestUserStatusActionAuthorization(c)
suite.TestGroupStatusActionAuthorization(c)
suite.TestUserPolicyEscalationBug(c)
suite.TestPolicyCreate(c)
suite.TestServiceAccountBareARNPolicyRejected(c)
@@ -413,6 +434,106 @@ func (s *TestSuiteIAM) TestUserStatusActionAuthorization(c *check) {
}
}
func (s *TestSuiteIAM) TestGroupStatusActionAuthorization(c *check) {
ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
defer cancel()
var createdUsers []string
var createdPolicies []string
group := getRandomBucketName()
var groupCreated bool
defer func() {
if groupCreated {
if err := s.adm.UpdateGroupMembers(ctx, madmin.GroupAddRemove{
Group: group,
Members: createdUsers[:1],
IsRemove: true,
}); err != nil {
c.Errorf("unable to remove group member: %v", err)
}
if err := s.adm.UpdateGroupMembers(ctx, madmin.GroupAddRemove{Group: group, IsRemove: true}); err != nil {
c.Errorf("unable to remove test group: %v", err)
}
}
for _, user := range createdUsers {
if err := s.adm.RemoveUser(ctx, user); err != nil {
c.Errorf("unable to remove test user %s: %v", user, err)
}
}
for _, policyName := range createdPolicies {
if err := s.adm.RemoveCannedPolicy(ctx, policyName); err != nil {
c.Errorf("unable to remove test policy %s: %v", policyName, err)
}
}
}()
createUser := func() (string, string) {
accessKey, secretKey := mustGenerateCredentials(c)
if err := s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled); err != nil {
c.Fatalf("unable to create test user: %v", err)
}
createdUsers = append(createdUsers, accessKey)
return accessKey, secretKey
}
targetAccessKey, _ := createUser()
if err := s.adm.UpdateGroupMembers(ctx, madmin.GroupAddRemove{
Group: group,
Members: []string{targetAccessKey},
}); err != nil {
c.Fatalf("unable to create test group: %v", err)
}
groupCreated = true
createStatusClient := func(action policy.AdminAction) *madmin.AdminClient {
accessKey, secretKey := createUser()
policyName := getRandomBucketName()
policyBytes := fmt.Appendf(nil, `{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["%s"]
}]
}`, action)
if err := s.adm.AddCannedPolicy(ctx, policyName, policyBytes); err != nil {
c.Fatalf("unable to add group status policy: %v", err)
}
createdPolicies = append(createdPolicies, policyName)
if _, err := s.adm.AttachPolicy(ctx, madmin.PolicyAssociationReq{
Policies: []string{policyName},
User: accessKey,
}); err != nil {
c.Fatalf("unable to attach group status policy: %v", err)
}
client, err := madmin.NewWithOptions(s.endpoint, &madmin.Options{
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
Secure: s.secure,
})
if err != nil {
c.Fatalf("unable to create group status admin client: %v", err)
}
client.SetCustomTransport(s.TestSuiteCommon.client.Transport)
return client
}
disableClient := createStatusClient(policy.DisableGroupAdminAction)
if err := disableClient.SetGroupStatus(ctx, group, madmin.GroupDisabled); err != nil {
c.Fatalf("DisableGroup-only client could not disable a group: %v", err)
}
if err := disableClient.SetGroupStatus(ctx, group, madmin.GroupEnabled); err == nil || madmin.ToErrorResponse(err).Code != "AccessDenied" {
c.Fatalf("DisableGroup-only client unexpectedly enabled a group: %v", err)
}
enableClient := createStatusClient(policy.EnableGroupAdminAction)
if err := enableClient.SetGroupStatus(ctx, group, madmin.GroupEnabled); err != nil {
c.Fatalf("EnableGroup-only client could not enable a group: %v", err)
}
if err := enableClient.SetGroupStatus(ctx, group, madmin.GroupDisabled); err == nil || madmin.ToErrorResponse(err).Code != "AccessDenied" {
c.Fatalf("EnableGroup-only client unexpectedly disabled a group: %v", err)
}
}
func (s *TestSuiteIAM) TestUserPolicyEscalationBug(c *check) {
ctx, cancel := context.WithTimeout(context.Background(), testDefaultTimeout)
defer cancel()
+1 -2
View File
@@ -777,8 +777,7 @@ func generateListObjectsV2Response(ctx context.Context, bucket, prefix, token, n
type metaCheckFn = func(name string, action policy.Action) (s3Err APIErrorCode)
// generates CopyObjectResponse from the committed object information.
func generateCopyObjectResponse(oi ObjectInfo, h http.Header) CopyObjectResponse {
cs, _ := oi.decryptChecksums(0, h)
func generateCopyObjectResponse(oi ObjectInfo, cs map[string]string) CopyObjectResponse {
return CopyObjectResponse{
ETag: "\"" + oi.ETag + "\"",
LastModified: amztime.ISO8601Format(oi.ModTime.UTC()),
+4 -7
View File
@@ -37,6 +37,7 @@ import (
"syscall"
"time"
"unicode"
"unicode/utf8"
"github.com/dustin/go-humanize"
fcolor "github.com/fatih/color"
@@ -542,21 +543,17 @@ func (e envKV) String() string {
}
func isValidEnvName(name string) bool {
if name == "" || !isEnvNameStart(name[0]) {
if name == "" || !utf8.ValidString(name) {
return false
}
for i := 1; i < len(name); i++ {
if !isEnvNameStart(name[i]) && (name[i] < '0' || name[i] > '9') {
for _, ch := range name {
if ch == '=' || unicode.IsSpace(ch) || !unicode.IsGraphic(ch) {
return false
}
}
return true
}
func isEnvNameStart(ch byte) bool {
return ch == '_' || ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z'
}
func trimExportPrefix(envEntry string) string {
rest, ok := strings.CutPrefix(envEntry, "export")
if !ok || rest == "" {
+42 -12
View File
@@ -22,8 +22,11 @@ import (
"fmt"
"os"
"reflect"
"slices"
"strings"
"testing"
"github.com/minio/minio/internal/config"
)
func Test_readFromSecret(t *testing.T) {
@@ -240,6 +243,19 @@ func Test_minioEnvironFromFileWhitespaceAndValidation(t *testing.T) {
{Key: "_VALID_2", Value: "value"},
},
},
{
name: "named target punctuation and unicode",
content: "MINIO_NOTIFY_WEBHOOK_ENABLE_my-hook=off\n" +
"MINIO_NOTIFY_WEBHOOK_ENABLE_site.eu=off\n" +
"MINIO_NOTIFY_WEBHOOK_ENABLE_team:blue=off\n" +
"MINIO_NOTIFY_WEBHOOK_ENABLE_目标=off",
want: []envKV{
{Key: "MINIO_NOTIFY_WEBHOOK_ENABLE_my-hook", Value: "off"},
{Key: "MINIO_NOTIFY_WEBHOOK_ENABLE_site.eu", Value: "off"},
{Key: "MINIO_NOTIFY_WEBHOOK_ENABLE_team:blue", Value: "off"},
{Key: "MINIO_NOTIFY_WEBHOOK_ENABLE_目标", Value: "off"},
},
},
{
name: "missing separator redacts the line",
content: "MINIO_ROOT_PASSWORD=valid\nsuper-secret-without-equals",
@@ -255,18 +271,12 @@ func Test_minioEnvironFromFileWhitespaceAndValidation(t *testing.T) {
errExcludes: "empty-name-secret",
},
{
name: "digit leading name",
content: "1MINIO_ROOT_USER=digit-leading-secret",
errLine: 1,
errContains: `invalid environment variable name "1MINIO_ROOT_USER"`,
errExcludes: "digit-leading-secret",
},
{
name: "hyphenated name",
content: "MINIO-ROOT-USER=hyphen-secret",
errLine: 1,
errContains: `invalid environment variable name "MINIO-ROOT-USER"`,
errExcludes: "hyphen-secret",
name: "os compatible leading digit and punctuation",
content: "1MINIO_ROOT_USER=digit-leading-secret\n-MINIO-ROOT-USER=hyphen-secret",
want: []envKV{
{Key: "1MINIO_ROOT_USER", Value: "digit-leading-secret"},
{Key: "-MINIO-ROOT-USER", Value: "hyphen-secret"},
},
},
{
name: "whitespace in name",
@@ -282,6 +292,13 @@ func Test_minioEnvironFromFileWhitespaceAndValidation(t *testing.T) {
errContains: "invalid environment variable name",
errExcludes: "nul-name-secret",
},
{
name: "format character in name",
content: "MINIO\u200bROOT=format-secret",
errLine: 1,
errContains: "invalid environment variable name",
errExcludes: "format-secret",
},
{
name: "NUL in value",
content: "MINIO_ROOT_USER=before\x00nul-value-secret",
@@ -342,3 +359,16 @@ func Test_minioEnvironFromFileWhitespaceAndValidation(t *testing.T) {
})
}
}
func TestConfigEnvFileNamedTargetDiscovery(t *testing.T) {
key := "MINIO_NOTIFY_WEBHOOK_ENABLE_my-hook"
t.Setenv(key, "off")
targets, err := (config.Config{}).GetAvailableTargets(config.NotifyWebhookSubSys)
if err != nil {
t.Fatal(err)
}
if !slices.Contains(targets, "my-hook") {
t.Fatalf("named target %q not discovered from %s: %v", "my-hook", key, targets)
}
}
+66
View File
@@ -420,8 +420,74 @@ func testAPICompleteMultipartChecksumTypeMismatch(obj ObjectLayer, instanceType,
t.Fatalf("%s: object was created despite a rejected checksum type", instanceType)
}
})
t.Run(test.name+"-type-only", func(t *testing.T) {
objectName := "type-mismatch/type-only-" + test.name
uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName,
typ.String(), test.createdType)
etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData)
partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])}
rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS,
map[string]string{xhttp.AmzChecksumType: test.providedType})
if rec.Code != http.StatusBadRequest {
t.Fatalf("%s: checksum type-only mismatch returned %d, want 400", instanceType, rec.Code)
}
apiErr := apiError(t, rec)
if apiErr.Code != "BadDigest" {
t.Fatalf("%s: expected BadDigest, got %q", instanceType, apiErr.Code)
}
wantMessage := fmt.Sprintf("The checksum type %s does not match the multipart upload checksum type %s.", test.providedType, test.createdType)
if apiErr.Message != wantMessage {
t.Fatalf("%s: expected message %q, got %q", instanceType, wantMessage, apiErr.Message)
}
if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil {
t.Fatalf("%s: object was created despite a rejected checksum type-only assertion", instanceType)
}
})
}
for _, test := range []struct {
name string
providedType string
withChecksum bool
}{
{name: "unknown-type-only", providedType: "NOT_A_TYPE"},
{name: "lowercase-type-only", providedType: "full_object"},
{name: "unknown-with-checksum", providedType: "NOT_A_TYPE", withChecksum: true},
} {
t.Run(test.name, func(t *testing.T) {
objectName := "type-mismatch/invalid-" + test.name
uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName,
typ.String(), xhttp.AmzChecksumTypeComposite)
etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData)
partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])}
headers := map[string]string{xhttp.AmzChecksumType: test.providedType}
if test.withChecksum {
headers[typ.Key()] = mustChecksum(t, typ, full)
}
rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS, headers)
if rec.Code != http.StatusBadRequest || apiErrorCode(t, rec) != "InvalidArgument" {
t.Fatalf("%s: invalid checksum type returned %d %s", instanceType, rec.Code, rec.Body.String())
}
if _, err := obj.GetObjectInfo(t.Context(), bucketName, objectName, ObjectOptions{}); err == nil {
t.Fatalf("%s: object was created despite an invalid checksum type", instanceType)
}
})
}
t.Run("matching-type-only", func(t *testing.T) {
objectName := "type-mismatch/matching-type-only"
uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName,
typ.String(), xhttp.AmzChecksumTypeComposite)
etags := uploadPartsHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, typ, partData)
partCS := []string{mustChecksum(t, typ, partData[0]), mustChecksum(t, typ, partData[1])}
rec := completeMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName, uploadID, etags, partCS,
map[string]string{xhttp.AmzChecksumType: xhttp.AmzChecksumTypeComposite})
if rec.Code != http.StatusOK {
t.Fatalf("%s: matching checksum type-only assertion returned %d %s", instanceType, rec.Code, rec.Body.String())
}
})
t.Run("omitted-type-is-not-composite", func(t *testing.T) {
objectName := "type-mismatch/omitted-type"
uploadID := newMultipartUploadHTTP(t, apiRouter, credentials, bucketName, objectName,
+11 -3
View File
@@ -1173,9 +1173,9 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
var checksumType hash.ChecksumType
if cs := fi.Metadata[hash.MinIOMultipartChecksum]; cs != "" {
checksumType = hash.NewChecksumType(cs, fi.Metadata[hash.MinIOMultipartChecksumType])
expectedType := checksumType | hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
if opts.WantChecksum != nil {
providedType := opts.WantChecksum.Type | hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
expectedType := checksumType | hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
if providedType.Base() != expectedType.Base() {
return oi, InvalidArgument{
Bucket: bucket,
@@ -1183,8 +1183,16 @@ func (er erasureObjects) CompleteMultipartUpload(ctx context.Context, bucket str
Err: fmt.Errorf("checksum algorithm mismatch. got %q expected %q", providedType.String(), expectedType.String()),
}
}
if opts.wantChecksumTypeSet && providedType.ObjType() != expectedType.ObjType() {
return oi, completeMultipartChecksumTypeMismatch(providedType.ObjType(), expectedType.ObjType())
}
if opts.wantChecksumType != "" {
providedObjectType := opts.wantChecksumType
// CRC64NVME is always canonicalized to FULL_OBJECT. Preserve this
// behavior until its exact AWS wire semantics have been probed.
if checksumType.Base().Is(hash.ChecksumCRC64NVME) {
providedObjectType = xhttp.AmzChecksumTypeFullObject
}
if providedObjectType != expectedType.ObjType() {
return oi, completeMultipartChecksumTypeMismatch(opts.wantChecksumType, expectedType.ObjType())
}
}
checksumType |= hash.ChecksumMultipart | hash.ChecksumIncludesMultipart
+2 -2
View File
@@ -84,8 +84,8 @@ type ObjectOptions struct {
Expiration ExpirationOptions
LifecycleAuditEvent lcAuditEvent
WantChecksum *hash.Checksum // x-amz-checksum-XXX checksum sent to PutObject/ CompleteMultipartUpload.
wantChecksumTypeSet bool // x-amz-checksum-type was explicitly set on CompleteMultipartUpload.
WantChecksum *hash.Checksum // x-amz-checksum-XXX checksum sent to PutObject/ CompleteMultipartUpload.
wantChecksumType string // explicit x-amz-checksum-type value on CompleteMultipartUpload.
WantServerSideChecksumType hash.ChecksumType // if set, we compute a server-side checksum of this type
+7 -1
View File
@@ -469,11 +469,17 @@ func completeMultipartOpts(ctx context.Context, r *http.Request, bucket, object
}
}
opts.wantChecksumType = r.Header.Get(xhttp.AmzChecksumType)
switch opts.wantChecksumType {
case "", xhttp.AmzChecksumTypeComposite, xhttp.AmzChecksumTypeFullObject:
default:
return opts, hash.ErrInvalidChecksum
}
opts.WantChecksum, err = hash.GetContentChecksum(r.Header)
if err != nil {
return opts, err
}
opts.wantChecksumTypeSet = r.Header.Get(xhttp.AmzChecksumType) != ""
opts.MTime = mtime
opts.UserDefined = make(map[string]string)
// Transfer SSEC key in opts.EncryptFn
+38
View File
@@ -387,6 +387,44 @@ func testAPICopyObjectServerSideChecksumEncryption(obj ObjectLayer, instanceType
}
})
}
oldKey := bytes.Repeat([]byte{0x31}, 32)
oldKeyMD5 := md5.Sum(oldKey)
newKey := bytes.Repeat([]byte{0x42}, 32)
newKeyMD5 := md5.Sum(newKey)
encryptedSource := "copy-checksum/sse-c-different-key-source.bin"
putCopyChecksumSource(t, apiRouter, credentials, bucketName, encryptedSource, data, map[string]string{
xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES,
xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(oldKey),
xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(oldKeyMD5[:]),
})
destination := "copy-checksum/sse-c-different-key-destination.bin"
rec := copyChecksumRequest(t, apiRouter, credentials, bucketName, encryptedSource, destination, map[string]string{
xhttp.AmzChecksumAlgo: hash.ChecksumCRC32.String(),
xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES,
xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(newKey),
xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(newKeyMD5[:]),
xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm: xhttp.AmzEncryptionAES,
xhttp.AmzServerSideEncryptionCopyCustomerKey: base64.StdEncoding.EncodeToString(oldKey),
xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5: base64.StdEncoding.EncodeToString(oldKeyMD5[:]),
})
if rec.Code != http.StatusOK {
t.Fatalf("%s: different-key SSE-C CopyObject failed: %d %s", instanceType, rec.Code, rec.Body.String())
}
assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data)
if got, want := rec.Header().Get(hash.ChecksumCRC32.Key()), mustChecksum(t, hash.ChecksumCRC32, data); got != want {
t.Fatalf("%s: different-key SSE-C response header checksum %q, want %q", instanceType, got, want)
}
if got := rec.Header().Get(xhttp.AmzChecksumType); got != xhttp.AmzChecksumTypeFullObject {
t.Fatalf("%s: different-key SSE-C response checksum type %q, want %q", instanceType, got, xhttp.AmzChecksumTypeFullObject)
}
newKeyHeaders := http.Header{
xhttp.AmzServerSideEncryptionCustomerAlgorithm: []string{xhttp.AmzEncryptionAES},
xhttp.AmzServerSideEncryptionCustomerKey: []string{base64.StdEncoding.EncodeToString(newKey)},
xhttp.AmzServerSideEncryptionCustomerKeyMD5: []string{base64.StdEncoding.EncodeToString(newKeyMD5[:])},
}
assertCopyChecksum(t, obj, bucketName, destination, hash.ChecksumCRC32, data, false, newKeyHeaders)
})
}
+2
View File
@@ -150,6 +150,7 @@ func testAPICopyObjectSSECKeyRotationKeepsCompressionState(obj ObjectLayer, inst
newMD5 := md5.Sum(newKey)
putCopyChecksumSource(t, apiRouter, credentials, bucketName, object, data, map[string]string{
xhttp.AmzChecksumCRC32: mustChecksum(t, hash.ChecksumCRC32, data),
xhttp.AmzServerSideEncryptionCustomerAlgorithm: xhttp.AmzEncryptionAES,
xhttp.AmzServerSideEncryptionCustomerKey: base64.StdEncoding.EncodeToString(oldKey),
xhttp.AmzServerSideEncryptionCustomerKeyMD5: base64.StdEncoding.EncodeToString(oldMD5[:]),
@@ -172,6 +173,7 @@ func testAPICopyObjectSSECKeyRotationKeepsCompressionState(obj ObjectLayer, inst
if rec.Code != http.StatusOK {
t.Fatalf("%s: key rotation failed: %d %s", instanceType, rec.Code, rec.Body.String())
}
assertCopyChecksumResponse(t, rec, hash.ChecksumCRC32, data)
after, err := obj.GetObjectInfo(t.Context(), bucketName, object, ObjectOptions{})
if err != nil {
t.Fatal(err)
+5 -1
View File
@@ -353,6 +353,11 @@ func isETagEqual(left, right string) bool {
// upon a success Put/Copy/CompleteMultipart/Delete requests
// to activate delete only headers set delete as true
func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, del bool, h http.Header) {
cs, _ := objInfo.decryptChecksums(0, h)
setPutObjHeadersWithChecksum(w, objInfo, del, cs)
}
func setPutObjHeadersWithChecksum(w http.ResponseWriter, objInfo ObjectInfo, del bool, cs map[string]string) {
// We must not use the http.Header().Set method here because some (broken)
// clients expect the ETag header key to be literally "ETag" - not "Etag" (case-sensitive).
// Therefore, we have to set the ETag directly as map entry.
@@ -374,7 +379,6 @@ func setPutObjHeaders(w http.ResponseWriter, objInfo ObjectInfo, del bool, h htt
lc.SetPredictionHeaders(w, objInfo.ToLifecycleOpts())
}
}
cs, _ := objInfo.decryptChecksums(0, h)
hash.AddChecksumHeader(w, cs)
}
+12 -2
View File
@@ -1107,6 +1107,14 @@ func cloneRequestWithoutCopyReplicationHeaders(r *http.Request) *http.Request {
return clone
}
func copyDestinationSSEHeaders(h http.Header) http.Header {
dst := h.Clone()
dst.Del(xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm)
dst.Del(xhttp.AmzServerSideEncryptionCopyCustomerKey)
dst.Del(xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5)
return dst
}
// getRemoteInstanceTransport contains a roundtripper for external (not peers) servers
var remoteInstanceTransport atomic.Value
@@ -1816,14 +1824,16 @@ func (api objectAPIHandlers) CopyObjectHandler(w http.ResponseWriter, r *http.Re
origETag := objInfo.ETag
objInfo.ETag = getDecryptedETag(r.Header, objInfo, false)
response := generateCopyObjectResponse(objInfo, r.Header)
dstHeaders := copyDestinationSSEHeaders(r.Header)
checksums, _ := objInfo.decryptChecksums(0, dstHeaders)
response := generateCopyObjectResponse(objInfo, checksums)
encodedSuccessResponse := encodeResponse(response)
if dsc := mustReplicate(ctx, dstBucket, dstObject, objInfo.getMustReplicateOptions(replication.ObjectReplicationType, dstOpts)); dsc.ReplicateAny() {
scheduleReplication(ctx, objInfo, objectAPI, dsc, replication.ObjectReplicationType)
}
setPutObjHeaders(w, objInfo, false, r.Header)
setPutObjHeadersWithChecksum(w, objInfo, false, checksums)
// We must not use the http.Header().Set method here because some (broken)
// clients expect the x-amz-copy-source-version-id header key to be literally
// "x-amz-copy-source-version-id"- not in canonicalized form, preserve it.
@@ -40,7 +40,7 @@ import (
xhttp "github.com/minio/minio/internal/http"
)
const federatedTestUserAgent = "MinIO (linux; amd64) minio-go/v7.0.99 minio-federated/RELEASE.TEST"
const federatedTestUserAgent = "MinIO (linux; amd64) minio-go/v7.3.1 minio-federated/RELEASE.TEST"
func TestAPIFederatedUploadPartChecksumResponse(t *testing.T) {
defer DetectTestLeak(t)()
+8
View File
@@ -4,6 +4,14 @@ This document summarizes fork-specific security fixes and closely related upgrad
Entries carry a CVE identifier where one exists. Where none does, they carry a fork-local `SN-<year>-<sequence>` identifier so that a finding without a CVE can still be referenced stably from release notes, commits and issues. An `SN-` identifier is **not** a CVE and is not registered in any vulnerability database; it is deliberately not written in CVE form so that scanners do not mistake it for one. Upstream `minio/minio` is archived, so for findings in inherited code there is no upstream maintainer to coordinate a CVE assignment with. `SN-2026-001` is the streaming-flush regression in `trackingResponseWriter`, which is a reliability defect rather than a security one and is tracked in the release notes rather than here.
## Inherited upstream advisory baseline
The first Silo community release was cut from upstream history that already contained the following security fix. Upstream and Silo links are both recorded even when the fork preserves the same commit object and SHA; that identity is the inheritance evidence, not a claim that Silo independently reimplemented the patch.
| ID | Upstream remediation | Silo inheritance | Regression evidence | Release / operator note |
| :-- | :-- | :-- | :-- | :-- |
| [CVE-2025-62506](https://github.com/advisories/GHSA-jjjj-jwhf-8rgr) | [minio/minio#21642](https://github.com/minio/minio/pull/21642), merged as [`c1a49490`](https://github.com/minio/minio/commit/c1a49490c78e9c3ebcad86ba0662319138ace190) | The same commit object is present as [`pgsty/silo@c1a49490`](https://github.com/pgsty/silo/commit/c1a49490c78e9c3ebcad86ba0662319138ace190) | The inherited [service-account](https://github.com/pgsty/silo/blob/c1a49490c78e9c3ebcad86ba0662319138ace190/cmd/admin-handlers-users_test.go#L211-L212) and [STS](https://github.com/pgsty/silo/blob/c1a49490c78e9c3ebcad86ba0662319138ace190/cmd/sts-handlers_test.go#L45-L46) regression groups run for root and non-root parents through `go test ./cmd` | Resets `DenyOnly` while evaluating a restricted session policy so service or STS accounts cannot mint an unrestricted child service account. Upstream first fixed this in [`RELEASE.2025-10-15T17-29-55Z`](https://github.com/minio/minio/releases/tag/RELEASE.2025-10-15T17-29-55Z); every Silo community release, beginning with [`RELEASE.2025-12-03T12-00-00Z`](https://github.com/pgsty/silo/releases/tag/RELEASE.2025-12-03T12-00-00Z), contains it. Operators migrating from an older upstream build should upgrade and audit service accounts created by restricted service or STS identities. |
## Advisories since `RELEASE.2026-03-21T00-00-00Z`
| ID | Fixed by | Affected area | Remote exploitability | Summary | Upgrade / workaround notes |