mirror of
https://github.com/pgsty/minio.git
synced 2026-11-12 10:35:33 +03:00
34ba7cc1ff
Harden IAM/bucket/import/replication policy validation, enforce tag and version authorization across read/copy/list/archive endpoints, bind SigV4 credential-scope dates and streaming trailer limits, validate HTTP SSE-KMS contexts as strict UTF-8 JSON, add release-commit and coordinated dependency gates, and pin silo-pkg v3.16.1, Console v2.7.1, mcli 20261112, Go 1.27.4, and Helm chart 7.0.5. Signed-off-by: Feng Ruohang <rh@vonng.com>
160 lines
4.8 KiB
Go
160 lines
4.8 KiB
Go
// Copyright (c) 2026 PGSTY
|
|
//
|
|
// This file is part of MinIO 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.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
|
|
"github.com/pgsty/silo-pkg/v3/policy"
|
|
)
|
|
|
|
type strictIAMStatementWire struct {
|
|
SID policy.ID `json:"Sid,omitempty"`
|
|
Effect policy.Effect `json:"Effect"`
|
|
Actions json.RawMessage `json:"Action,omitempty"`
|
|
NotActions json.RawMessage `json:"NotAction,omitempty"`
|
|
Resources json.RawMessage `json:"Resource,omitempty"`
|
|
NotResources json.RawMessage `json:"NotResource,omitempty"`
|
|
Conditions json.RawMessage `json:"Condition,omitempty"`
|
|
}
|
|
|
|
type strictIAMPolicyWire struct {
|
|
ID policy.ID `json:"ID,omitempty"`
|
|
Version string `json:"Version"`
|
|
Statements []strictIAMStatementWire `json:"Statement"`
|
|
}
|
|
|
|
type strictPrincipalWire struct {
|
|
AWS json.RawMessage `json:"AWS"`
|
|
}
|
|
|
|
type strictBucketStatementWire struct {
|
|
SID policy.ID `json:"Sid,omitempty"`
|
|
Effect policy.Effect `json:"Effect"`
|
|
Principal json.RawMessage `json:"Principal,omitempty"`
|
|
Actions json.RawMessage `json:"Action,omitempty"`
|
|
NotActions json.RawMessage `json:"NotAction,omitempty"`
|
|
Resources json.RawMessage `json:"Resource,omitempty"`
|
|
NotResources json.RawMessage `json:"NotResource,omitempty"`
|
|
Conditions json.RawMessage `json:"Condition,omitempty"`
|
|
}
|
|
|
|
type strictBucketPolicyWire struct {
|
|
ID policy.ID `json:"ID,omitempty"`
|
|
Version string `json:"Version"`
|
|
Statements []strictBucketStatementWire `json:"Statement"`
|
|
}
|
|
|
|
func validateStrictIAMPolicyWire(data []byte) ([]byte, error) {
|
|
trimmed := bytes.TrimSpace(data)
|
|
if len(trimmed) == 0 || trimmed[0] != '{' {
|
|
return nil, policy.Errorf("policy must be a JSON object")
|
|
}
|
|
var wire strictIAMPolicyWire
|
|
dec := json.NewDecoder(bytes.NewReader(trimmed))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&wire); err != nil {
|
|
return nil, policy.Errorf("%w", err)
|
|
}
|
|
var extra any
|
|
if err := dec.Decode(&extra); err != io.EOF {
|
|
if err == nil {
|
|
return nil, policy.Errorf("unexpected trailing JSON data in policy")
|
|
}
|
|
return nil, policy.Errorf("%w", err)
|
|
}
|
|
return trimmed, nil
|
|
}
|
|
|
|
func parsePolicyStrictBytes(data []byte) (*policy.Policy, error) {
|
|
trimmed, err := validateStrictIAMPolicyWire(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p, err := policy.ParseConfigStrict(bytes.NewReader(trimmed))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if p.IsEmpty() {
|
|
return nil, policy.Errorf("empty policies are not allowed")
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func parseSessionPolicyStrictBytes(data []byte) (*policy.Policy, error) {
|
|
trimmed, err := validateStrictIAMPolicyWire(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return policy.ParseConfigStrict(bytes.NewReader(trimmed))
|
|
}
|
|
|
|
// Snapshot imports retain supported historical policy semantics while rejecting
|
|
// unknown JSON fields that could silently discard restrictions.
|
|
func parsePolicySnapshotBytes(data []byte) (*policy.Policy, error) {
|
|
trimmed, err := validateStrictIAMPolicyWire(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return policy.ParseConfig(bytes.NewReader(trimmed))
|
|
}
|
|
|
|
func validateStrictPrincipalWire(raw json.RawMessage) error {
|
|
trimmed := bytes.TrimSpace(raw)
|
|
if len(trimmed) == 0 || trimmed[0] != '{' {
|
|
return nil
|
|
}
|
|
var pw strictPrincipalWire
|
|
dec := json.NewDecoder(bytes.NewReader(trimmed))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&pw); err != nil {
|
|
return policy.Errorf("%w", err)
|
|
}
|
|
var extra any
|
|
if err := dec.Decode(&extra); err != io.EOF {
|
|
if err == nil {
|
|
return policy.Errorf("unexpected trailing JSON data in principal")
|
|
}
|
|
return policy.Errorf("%w", err)
|
|
}
|
|
if len(bytes.TrimSpace(pw.AWS)) == 0 {
|
|
return policy.Errorf("invalid principal '%s'", string(raw))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseBucketPolicyStrictBytes(data []byte, bucket string) (*policy.BucketPolicy, error) {
|
|
trimmed := bytes.TrimSpace(data)
|
|
if len(trimmed) == 0 || trimmed[0] != '{' {
|
|
return nil, policy.Errorf("bucket policy must be a JSON object")
|
|
}
|
|
var wire strictBucketPolicyWire
|
|
dec := json.NewDecoder(bytes.NewReader(trimmed))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&wire); err != nil {
|
|
return nil, policy.Errorf("%w", err)
|
|
}
|
|
var extra any
|
|
if err := dec.Decode(&extra); err != io.EOF {
|
|
if err == nil {
|
|
return nil, policy.Errorf("unexpected trailing JSON data in bucket policy")
|
|
}
|
|
return nil, policy.Errorf("%w", err)
|
|
}
|
|
for _, st := range wire.Statements {
|
|
if err := validateStrictPrincipalWire(st.Principal); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return policy.ParseBucketPolicyConfig(bytes.NewReader(trimmed), bucket)
|
|
}
|