mirror of
https://github.com/pgsty/minio.git
synced 2026-11-11 10:05:34 +03:00
fix(security): close release blockers
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
// Copyright (c) 2026 Feng Ruohang
|
||||
//
|
||||
// This file is part of Silo Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/minio/internal/crypto"
|
||||
"github.com/minio/minio/internal/hash/sha256"
|
||||
xioutil "github.com/minio/minio/internal/ioutil"
|
||||
)
|
||||
|
||||
type replicaContentGuardFn func(context.Context, string, string, ObjectInfo, func() (*GetObjectReader, error)) error
|
||||
|
||||
// verifyLockedReplica compares the actual data, never a sender's ETag or
|
||||
// checksum assertion. A peer may retransmit a protected version to update its
|
||||
// metadata, but may not replace its content. Call under the object write lock.
|
||||
func verifyLockedReplica(ctx context.Context, incoming, stored ObjectInfo, openIncoming, openStored func() (*GetObjectReader, error)) error {
|
||||
if !enforceRetentionForDeletion(ctx, stored) {
|
||||
return nil
|
||||
}
|
||||
locked := ObjectLocked{Bucket: stored.Bucket, Object: stored.Name, VersionID: stored.VersionID}
|
||||
if storedObjectLockState(stored.UserDefined).complianceWouldWeaken(storedObjectLockState(incoming.UserDefined)) {
|
||||
return locked
|
||||
}
|
||||
inSSEC, oldSSEC := crypto.SSEC.IsEncrypted(incoming.UserDefined), crypto.SSEC.IsEncrypted(stored.UserDefined)
|
||||
if inSSEC != oldSSEC {
|
||||
return locked
|
||||
}
|
||||
if inSSEC {
|
||||
inSize, inErr := incoming.GetActualSize()
|
||||
oldSize, oldErr := stored.GetActualSize()
|
||||
if inErr != nil || oldErr != nil || inSize != oldSize {
|
||||
return locked
|
||||
}
|
||||
// Without the customer key, compare ciphertext and its interpretation.
|
||||
// Equal ciphertext with a changed seal or multipart framing is not the
|
||||
// same object, even if the supplied ETag and size are unchanged.
|
||||
for _, key := range []string{
|
||||
crypto.MetaIV, crypto.MetaAlgorithm, crypto.MetaSealedKeySSEC,
|
||||
ReservedMetadataPrefix + "compression",
|
||||
} {
|
||||
if incoming.UserDefined[key] != stored.UserDefined[key] {
|
||||
return locked
|
||||
}
|
||||
}
|
||||
if crypto.IsMultiPart(incoming.UserDefined) != crypto.IsMultiPart(stored.UserDefined) || incoming.isMultipart() != stored.isMultipart() {
|
||||
return locked
|
||||
}
|
||||
if crypto.IsMultiPart(incoming.UserDefined) {
|
||||
if len(incoming.Parts) != len(stored.Parts) {
|
||||
return locked
|
||||
}
|
||||
for i, p := range incoming.Parts {
|
||||
old := stored.Parts[i]
|
||||
if p.Number != old.Number || p.Size != old.Size {
|
||||
return locked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
in, err := openIncoming()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
old, err := openStored()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer old.Close()
|
||||
ih, oh := sha256.New(), sha256.New()
|
||||
inSize, err := io.Copy(ih, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oldSize, err := io.Copy(oh, old)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if inSize != oldSize || !bytes.Equal(ih.Sum(nil), oh.Sum(nil)) {
|
||||
return locked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (er erasureObjects) guardReplicaContent(ctx context.Context, bucket, object string, incoming ObjectInfo, openIncoming func() (*GetObjectReader, error)) error {
|
||||
opts := ObjectOptions{VersionID: incoming.VersionID, NoLock: true, NoAuditLog: true, ReplicationRequest: true}
|
||||
if opts.VersionID == "" {
|
||||
opts.VersionID = nullVersionID
|
||||
}
|
||||
stored, err := er.getObjectInfo(ctx, bucket, object, opts)
|
||||
if isErrObjectNotFound(err) || isErrVersionNotFound(err) || isErrMethodNotAllowed(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return verifyLockedReplica(ctx, incoming, stored, openIncoming, func() (*GetObjectReader, error) {
|
||||
return er.GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, opts)
|
||||
})
|
||||
}
|
||||
|
||||
func (z *erasureServerPools) guardReplicaContent(ctx context.Context, bucket, object string, incoming ObjectInfo, openIncoming func() (*GetObjectReader, error)) error {
|
||||
opts := ObjectOptions{VersionID: incoming.VersionID, NoLock: true, NoAuditLog: true, ReplicationRequest: true}
|
||||
if opts.VersionID == "" {
|
||||
opts.VersionID = nullVersionID
|
||||
}
|
||||
copies, err := z.objectPoolInfos(ctx, bucket, object, opts)
|
||||
if isErrObjectNotFound(err) || isErrVersionNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, copy := range copies {
|
||||
if err := verifyLockedReplica(ctx, incoming, copy.ObjInfo, openIncoming, func() (*GetObjectReader, error) {
|
||||
return z.serverPools[copy.Index].GetObjectNInfo(ctx, bucket, object, nil, http.Header{}, opts)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkReplicaContent reads the staged version before rename commits it. Its
|
||||
// data is decoded/decrypted just like a GET (raw ciphertext for SSE-C), so this
|
||||
// covers multipart, compression and destination SSE-S3/KMS re-encryption.
|
||||
// disks and metadata are in erasure shard order, as passed to renameData.
|
||||
func (er erasureObjects) checkReplicaContent(ctx context.Context, bucket, object, stageBucket, stageObject string, metadata []FileInfo, disks []StorageAPI, opts ObjectOptions) error {
|
||||
if (!opts.ReplicaLockReconcile && !opts.ReplicationRequest) || opts.DataMovement {
|
||||
return nil
|
||||
}
|
||||
var fi FileInfo
|
||||
for i, disk := range disks {
|
||||
if disk != nil {
|
||||
fi = metadata[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
incoming := fi.ToObjectInfo(bucket, object, opts.Versioned || opts.VersionSuspended)
|
||||
guard := opts.replicaContentGuard
|
||||
if guard == nil {
|
||||
guard = er.guardReplicaContent
|
||||
}
|
||||
return guard(ctx, bucket, object, incoming, func() (*GetObjectReader, error) {
|
||||
// A marker-only peer write does not reconcile the stored protection.
|
||||
// It therefore cannot replace a protected version, even with equal data.
|
||||
if !opts.ReplicaLockReconcile {
|
||||
return nil, ObjectLocked{Bucket: bucket, Object: decodeDirObject(object), VersionID: incoming.VersionID}
|
||||
}
|
||||
if incoming.IsCompressed() {
|
||||
// A replica may assert the source's logical size. A compressed GET
|
||||
// limits its output to that size, so verify it against the lengths
|
||||
// recorded while consuming the uploaded parts before comparing GETs.
|
||||
// Otherwise a forged zero size could hide a non-empty replacement.
|
||||
var actual int64
|
||||
for _, part := range incoming.Parts {
|
||||
actual += part.ActualSize
|
||||
}
|
||||
declared, err := incoming.GetActualSize()
|
||||
if err != nil || declared != actual {
|
||||
return nil, ObjectLocked{Bucket: bucket, Object: decodeDirObject(object), VersionID: incoming.VersionID}
|
||||
}
|
||||
}
|
||||
readOpts := ObjectOptions{NoDecryption: crypto.SSEC.IsEncrypted(incoming.UserDefined)}
|
||||
fn, off, length, err := NewGetObjectReader(nil, incoming, readOpts, http.Header{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The ordinary reader expects original drive order, not shard order.
|
||||
readDisks, readMeta := make([]StorageAPI, len(disks)), make([]FileInfo, len(metadata))
|
||||
for i, shard := range fi.Erasure.Distribution {
|
||||
readDisks[i], readMeta[i] = disks[shard-1], metadata[shard-1]
|
||||
readMeta[i].Erasure.Index = shard
|
||||
}
|
||||
pr, pw := xioutil.WaitPipe()
|
||||
go func() {
|
||||
pw.CloseWithError(er.getObjectWithFileInfo(ctx, stageBucket, stageObject, off, length, pw, fi, readMeta, readDisks))
|
||||
}()
|
||||
reader, err := fn(pr, http.Header{}, func() { pr.CloseWithError(nil) })
|
||||
if err != nil {
|
||||
pr.CloseWithError(err)
|
||||
}
|
||||
return reader, err
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user