2021-04-18 15:41:13 -04:00
// Copyright (c) 2015-2021 MinIO, Inc.
//
// 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.
//
// 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/>.
2015-10-04 19:31:07 -04:00
2016-08-18 19:23:42 -04:00
package cmd
2015-10-02 02:51:17 -04:00
import (
2021-03-21 01:16:30 -04:00
"bytes"
2015-10-02 02:51:17 -04:00
"encoding/json"
2016-11-30 21:30:59 -05:00
"errors"
2015-10-02 02:51:17 -04:00
"fmt"
2021-03-21 01:16:30 -04:00
"io"
2016-12-02 20:00:33 -05:00
"net/http"
2015-10-02 02:51:17 -04:00
"reflect"
2016-11-30 21:30:59 -05:00
"strconv"
2016-02-15 20:42:39 -05:00
"strings"
2015-10-02 02:51:17 -04:00
"time"
2021-03-21 01:16:30 -04:00
2023-09-25 15:35:25 -04:00
"github.com/minio/minio-go/v7/pkg/encrypt"
2021-03-25 16:57:57 -04:00
"github.com/minio/minio-go/v7/pkg/set"
2023-09-25 15:35:25 -04:00
xhttp "github.com/minio/minio/internal/http"
2024-09-23 15:35:41 -04:00
"github.com/minio/minio/internal/s3select/jstream"
2015-10-02 02:51:17 -04:00
)
2017-01-18 15:24:34 -05:00
// startWithConds - map which indicates if a given condition supports starts-with policy operator
var startsWithConds = map [ string ] bool {
2018-12-28 17:04:39 -05:00
"$acl" : true ,
"$bucket" : false ,
"$cache-control" : true ,
"$content-type" : true ,
"$content-disposition" : true ,
"$content-encoding" : true ,
"$expires" : true ,
"$key" : true ,
2017-01-18 15:24:34 -05:00
"$success_action_redirect" : true ,
"$redirect" : true ,
2021-07-13 16:33:03 -04:00
"$success_action_status" : true ,
2017-01-18 15:24:34 -05:00
"$x-amz-algorithm" : false ,
"$x-amz-credential" : false ,
"$x-amz-date" : false ,
}
// Add policy conditionals.
const (
policyCondEqual = "eq"
policyCondStartsWith = "starts-with"
policyCondContentLength = "content-length-range"
)
2015-10-02 02:51:17 -04:00
// toString - Safely convert interface to string without causing panic.
func toString ( val interface { } ) string {
switch v := val . ( type ) {
case string :
return v
2018-08-06 13:26:40 -04:00
default :
return ""
2015-10-02 02:51:17 -04:00
}
}
2016-12-02 20:00:33 -05:00
// toLowerString - safely convert interface to lower string
func toLowerString ( val interface { } ) string {
return strings . ToLower ( toString ( val ) )
}
2015-10-02 02:51:17 -04:00
// toInteger _ Safely convert interface to integer without causing panic.
2016-11-30 21:30:59 -05:00
func toInteger ( val interface { } ) ( int64 , error ) {
2015-10-02 02:51:17 -04:00
switch v := val . ( type ) {
2016-11-21 07:15:26 -05:00
case float64 :
2016-11-30 21:30:59 -05:00
return int64 ( v ) , nil
2016-11-21 07:15:26 -05:00
case int64 :
2016-11-30 21:30:59 -05:00
return v , nil
2016-11-21 07:15:26 -05:00
case int :
2016-11-30 21:30:59 -05:00
return int64 ( v ) , nil
case string :
i , err := strconv . Atoi ( v )
return int64 ( i ) , err
2018-08-06 13:26:40 -04:00
default :
return 0 , errors . New ( "Invalid number format" )
2015-10-02 02:51:17 -04:00
}
}
// isString - Safely check if val is of type string without causing panic.
func isString ( val interface { } ) bool {
2018-08-06 13:26:40 -04:00
_ , ok := val . ( string )
return ok
2015-10-02 02:51:17 -04:00
}
2016-10-25 02:47:03 -04:00
// ContentLengthRange - policy content-length-range field.
type contentLengthRange struct {
2016-11-21 07:15:26 -05:00
Min int64
Max int64
2016-10-25 02:47:03 -04:00
Valid bool // If content-length-range was part of policy
}
2015-10-02 02:51:17 -04:00
// PostPolicyForm provides strict static type conversion and validation for Amazon S3's POST policy JSON string.
type PostPolicyForm struct {
Expiration time . Time // Expiration date and time of the POST policy.
Conditions struct { // Conditional policy structure.
2019-09-22 17:20:49 -04:00
Policies [ ] struct {
2015-10-02 02:51:17 -04:00
Operator string
2019-09-22 17:20:49 -04:00
Key string
2015-10-02 02:51:17 -04:00
Value string
}
2016-10-25 02:47:03 -04:00
ContentLengthRange contentLengthRange
2015-10-02 02:51:17 -04:00
}
}
2021-03-21 01:16:30 -04:00
// implemented to ensure that duplicate keys in JSON
// are merged together into a single JSON key, also
// to remove any extraneous JSON bodies.
//
// Go stdlib doesn't support parsing JSON with duplicate
// keys, so we need to use this technique to merge the
// keys.
2021-03-25 16:57:57 -04:00
func sanitizePolicy ( r io . Reader ) ( io . Reader , error ) {
2021-03-21 01:16:30 -04:00
var buf bytes . Buffer
e := json . NewEncoder ( & buf )
2024-09-23 15:35:41 -04:00
d := jstream . NewDecoder ( r , 0 ) . ObjectAsKVS ( ) . MaxDepth ( 10 )
2021-03-25 16:57:57 -04:00
sset := set . NewStringSet ( )
2021-03-21 01:16:30 -04:00
for mv := range d . Stream ( ) {
2021-03-25 16:57:57 -04:00
var kvs jstream . KVS
if mv . ValueType == jstream . Object {
// This is a JSON object type (that preserves key order)
kvs = mv . Value . ( jstream . KVS )
for _ , kv := range kvs {
if sset . Contains ( kv . Key ) {
// Reject duplicate conditions or expiration.
return nil , fmt . Errorf ( "input policy has multiple %s, please fix your client code" , kv . Key )
}
sset . Add ( kv . Key )
}
e . Encode ( kvs )
}
2021-03-21 01:16:30 -04:00
}
return & buf , d . Err ( )
}
2019-07-10 01:18:43 -04:00
// parsePostPolicyForm - Parse JSON policy string into typed PostPolicyForm structure.
2021-03-25 16:57:57 -04:00
func parsePostPolicyForm ( r io . Reader ) ( PostPolicyForm , error ) {
reader , err := sanitizePolicy ( r )
2021-03-21 01:16:30 -04:00
if err != nil {
return PostPolicyForm { } , err
}
2021-03-25 16:57:57 -04:00
d := json . NewDecoder ( reader )
2021-03-21 01:16:30 -04:00
2015-10-02 02:51:17 -04:00
// Convert po into interfaces and
// perform strict type conversion using reflection.
var rawPolicy struct {
Expiration string ` json:"expiration" `
Conditions [ ] interface { } ` json:"conditions" `
}
2021-03-21 01:16:30 -04:00
d . DisallowUnknownFields ( )
if err := d . Decode ( & rawPolicy ) ; err != nil {
return PostPolicyForm { } , err
2015-10-02 02:51:17 -04:00
}
parsedPolicy := PostPolicyForm { }
// Parse expiry time.
2016-04-29 17:24:10 -04:00
parsedPolicy . Expiration , err = time . Parse ( time . RFC3339Nano , rawPolicy . Expiration )
if err != nil {
2021-03-21 01:16:30 -04:00
return PostPolicyForm { } , err
2015-10-02 02:51:17 -04:00
}
// Parse conditions.
for _ , val := range rawPolicy . Conditions {
switch condt := val . ( type ) {
case map [ string ] interface { } : // Handle key:value map types.
for k , v := range condt {
if ! isString ( v ) { // Pre-check value type.
// All values must be of type string.
2016-11-15 21:14:23 -05:00
return parsedPolicy , fmt . Errorf ( "Unknown type %s of conditional field value %s found in POST policy form" , reflect . TypeOf ( condt ) . String ( ) , condt )
2015-10-02 02:51:17 -04:00
}
// {"acl": "public-read" } is an alternate way to indicate - [ "eq", "$acl", "public-read" ]
// In this case we will just collapse this into "eq" for all use cases.
2019-09-22 17:20:49 -04:00
parsedPolicy . Conditions . Policies = append ( parsedPolicy . Conditions . Policies , struct {
2015-10-02 02:51:17 -04:00
Operator string
2019-09-22 17:20:49 -04:00
Key string
2015-10-02 02:51:17 -04:00
Value string
} {
2019-09-22 17:20:49 -04:00
policyCondEqual , "$" + strings . ToLower ( k ) , toString ( v ) ,
} )
2015-10-02 02:51:17 -04:00
}
case [ ] interface { } : // Handle array types.
if len ( condt ) != 3 { // Return error if we have insufficient elements.
2016-11-15 21:14:23 -05:00
return parsedPolicy , fmt . Errorf ( "Malformed conditional fields %s of type %s found in POST policy form" , condt , reflect . TypeOf ( condt ) . String ( ) )
2015-10-02 02:51:17 -04:00
}
2016-12-02 20:00:33 -05:00
switch toLowerString ( condt [ 0 ] ) {
2017-01-18 15:24:34 -05:00
case policyCondEqual , policyCondStartsWith :
2015-10-02 02:51:17 -04:00
for _ , v := range condt { // Pre-check all values for type.
if ! isString ( v ) {
// All values must be of type string.
2016-11-15 21:14:23 -05:00
return parsedPolicy , fmt . Errorf ( "Unknown type %s of conditional field value %s found in POST policy form" , reflect . TypeOf ( condt ) . String ( ) , condt )
2015-10-02 02:51:17 -04:00
}
}
2016-12-02 20:00:33 -05:00
operator , matchType , value := toLowerString ( condt [ 0 ] ) , toLowerString ( condt [ 1 ] ) , toString ( condt [ 2 ] )
2019-09-22 17:20:49 -04:00
if ! strings . HasPrefix ( matchType , "$" ) {
return parsedPolicy , fmt . Errorf ( "Invalid according to Policy: Policy Condition failed: [%s, %s, %s]" , operator , matchType , value )
}
parsedPolicy . Conditions . Policies = append ( parsedPolicy . Conditions . Policies , struct {
2015-10-02 02:51:17 -04:00
Operator string
2019-09-22 17:20:49 -04:00
Key string
2015-10-02 02:51:17 -04:00
Value string
} {
2019-09-22 17:20:49 -04:00
operator , matchType , value ,
} )
2017-01-18 15:24:34 -05:00
case policyCondContentLength :
2024-11-11 09:51:43 -05:00
minLen , err := toInteger ( condt [ 1 ] )
2016-11-30 21:30:59 -05:00
if err != nil {
return parsedPolicy , err
}
2024-11-11 09:51:43 -05:00
maxLen , err := toInteger ( condt [ 2 ] )
2016-11-30 21:30:59 -05:00
if err != nil {
return parsedPolicy , err
}
2016-11-21 07:15:26 -05:00
parsedPolicy . Conditions . ContentLengthRange = contentLengthRange {
2024-11-11 09:51:43 -05:00
Min : minLen ,
Max : maxLen ,
2016-11-21 07:15:26 -05:00
Valid : true ,
}
2015-10-02 02:51:17 -04:00
default :
// Condition should be valid.
2016-11-15 21:14:23 -05:00
return parsedPolicy , fmt . Errorf ( "Unknown type %s of conditional field value %s found in POST policy form" ,
reflect . TypeOf ( condt ) . String ( ) , condt )
2015-10-02 02:51:17 -04:00
}
default :
2016-11-15 21:14:23 -05:00
return parsedPolicy , fmt . Errorf ( "Unknown field %s of type %s found in POST policy form" ,
condt , reflect . TypeOf ( condt ) . String ( ) )
2015-10-02 02:51:17 -04:00
}
}
return parsedPolicy , nil
}
2016-02-15 20:42:39 -05:00
2024-01-18 02:03:17 -05:00
// checkPolicyCond returns a boolean to indicate if a condition is satisfied according
2016-12-02 20:00:33 -05:00
// to the passed operator
func checkPolicyCond ( op string , input1 , input2 string ) bool {
switch op {
2017-01-18 15:24:34 -05:00
case policyCondEqual :
2016-12-02 20:00:33 -05:00
return input1 == input2
2017-01-18 15:24:34 -05:00
case policyCondStartsWith :
2016-12-02 20:00:33 -05:00
return strings . HasPrefix ( input1 , input2 )
}
return false
}
2024-12-11 05:51:34 -05:00
// S3 docs: "Each form field that you specify in a form (except x-amz-signature, file, policy, and field names
// that have an x-ignore- prefix) must appear in the list of conditions."
// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
// keyInPolicyExceptions - list of keys that, when present in the form, can be missing in the conditions of the policy.
var keyInPolicyExceptions = map [ string ] bool {
xhttp . AmzSignature : true ,
"File" : true ,
"Policy" : true ,
// MinIO specific exceptions to the general S3 rule above.
encrypt . SseKmsKeyID : true ,
encrypt . SseEncryptionContext : true ,
encrypt . SseCustomerAlgorithm : true ,
encrypt . SseCustomerKey : true ,
encrypt . SseCustomerKeyMD5 : true ,
}
2016-03-12 19:08:15 -05:00
// checkPostPolicy - apply policy conditions and validate input values.
2024-12-11 05:51:34 -05:00
// Note that content-length-range is checked in the API handler function PostPolicyBucketHandler.
// formValues is the already-canonicalized form values from the POST request.
2019-03-05 15:10:47 -05:00
func checkPostPolicy ( formValues http . Header , postPolicyForm PostPolicyForm ) error {
2016-12-02 20:00:33 -05:00
// Check if policy document expiry date is still not reached
2017-03-18 14:28:41 -04:00
if ! postPolicyForm . Expiration . After ( UTCNow ( ) ) {
2019-03-05 15:10:47 -05:00
return fmt . Errorf ( "Invalid according to Policy: Policy expired" )
2016-02-15 20:42:39 -05:00
}
2024-12-11 05:51:34 -05:00
// mustFindInPolicy is a map to list all the keys that we must find in the policy as
// we process it below. At the end of checkPostPolicy function, if any key is left in
// this map, that's an error.
mustFindInPolicy := make ( map [ string ] [ ] string , len ( formValues ) )
for key , values := range formValues {
if keyInPolicyExceptions [ key ] || strings . HasPrefix ( key , "X-Ignore-" ) {
2023-09-25 15:35:25 -04:00
continue
2019-04-02 02:45:32 -04:00
}
2024-12-11 05:51:34 -05:00
mustFindInPolicy [ key ] = values
2019-04-02 02:45:32 -04:00
}
2016-12-02 20:00:33 -05:00
// Iterate over policy conditions and check them against received form fields
2019-09-22 17:20:49 -04:00
for _ , policy := range postPolicyForm . Conditions . Policies {
2016-12-02 20:00:33 -05:00
// Form fields names are in canonical format, convert conditions names
// to canonical for simplification purpose, so `$key` will become `Key`
2019-09-22 17:20:49 -04:00
formCanonicalName := http . CanonicalHeaderKey ( strings . TrimPrefix ( policy . Key , "$" ) )
2024-12-11 05:51:34 -05:00
2016-12-02 20:00:33 -05:00
// Operator for the current policy condition
2019-09-22 17:20:49 -04:00
op := policy . Operator
2024-12-11 05:51:34 -05:00
// Multiple values are not allowed for a single form field
if len ( mustFindInPolicy [ formCanonicalName ] ) >= 2 {
return fmt . Errorf ( "Invalid according to Policy: Policy Condition failed: [%s, %s, %s]. FormValues have multiple values: [%s]" , op , policy . Key , policy . Value , strings . Join ( mustFindInPolicy [ formCanonicalName ] , ", " ) )
2023-09-25 15:35:25 -04:00
}
2024-12-11 05:51:34 -05:00
2016-12-02 20:00:33 -05:00
// If the current policy condition is known
2019-09-22 17:20:49 -04:00
if startsWithSupported , condFound := startsWithConds [ policy . Key ] ; condFound {
2016-12-02 20:00:33 -05:00
// Check if the current condition supports starts-with operator
2017-01-18 15:24:34 -05:00
if op == policyCondStartsWith && ! startsWithSupported {
2019-03-05 15:10:47 -05:00
return fmt . Errorf ( "Invalid according to Policy: Policy Condition failed" )
2016-12-02 20:00:33 -05:00
}
// Check if current policy condition is satisfied
2024-12-11 05:51:34 -05:00
if ! checkPolicyCond ( op , formValues . Get ( formCanonicalName ) , policy . Value ) {
2019-03-05 15:10:47 -05:00
return fmt . Errorf ( "Invalid according to Policy: Policy Condition failed" )
}
2021-11-16 12:28:29 -05:00
} else if strings . HasPrefix ( policy . Key , "$x-amz-meta-" ) || strings . HasPrefix ( policy . Key , "$x-amz-" ) {
2016-12-02 20:00:33 -05:00
// This covers all conditions X-Amz-Meta-* and X-Amz-*
2021-11-16 12:28:29 -05:00
// Check if policy condition is satisfied
2024-12-11 05:51:34 -05:00
if ! checkPolicyCond ( op , formValues . Get ( formCanonicalName ) , policy . Value ) {
2021-11-16 12:28:29 -05:00
return fmt . Errorf ( "Invalid according to Policy: Policy Condition failed: [%s, %s, %s]" , op , policy . Key , policy . Value )
2016-12-02 20:00:33 -05:00
}
2016-02-15 20:42:39 -05:00
}
2024-12-11 05:51:34 -05:00
delete ( mustFindInPolicy , formCanonicalName )
2023-09-25 15:35:25 -04:00
}
2024-12-11 05:51:34 -05:00
// For SignV2 - Signature/AWSAccessKeyId fields do not need to be in the policy
2024-04-19 12:45:54 -04:00
if _ , ok := formValues [ xhttp . AmzSignatureV2 ] ; ok {
2024-12-11 05:51:34 -05:00
delete ( mustFindInPolicy , xhttp . AmzSignatureV2 )
for k := range mustFindInPolicy {
2024-05-06 06:52:41 -04:00
// case-insensitivity for AWSAccessKeyId
if strings . EqualFold ( k , xhttp . AmzAccessKeyID ) {
2024-12-11 05:51:34 -05:00
delete ( mustFindInPolicy , k )
2024-05-06 06:52:41 -04:00
break
}
}
2024-04-19 12:45:54 -04:00
}
2023-09-25 15:35:25 -04:00
2024-12-11 05:51:34 -05:00
// Check mustFindInPolicy to see if any key is left, if so, it was not found in policy and we return an error.
if len ( mustFindInPolicy ) != 0 {
logKeys := make ( [ ] string , 0 , len ( mustFindInPolicy ) )
for key := range mustFindInPolicy {
2023-09-25 15:35:25 -04:00
logKeys = append ( logKeys , key )
}
2024-07-10 10:18:44 -04:00
return fmt . Errorf ( "Each form field that you specify in a form must appear in the list of policy conditions. %q not specified in the policy." , strings . Join ( logKeys , ", " ) )
2016-02-15 20:42:39 -05:00
}
2016-12-02 20:00:33 -05:00
2019-03-05 15:10:47 -05:00
return nil
2016-02-15 20:42:39 -05:00
}