storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/postpolicyform.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2015, 2016, 2017 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package cmd
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"reflect"
    27  	"strconv"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/bcicen/jstream"
    32  	"github.com/minio/minio-go/v7/pkg/set"
    33  )
    34  
    35  // startWithConds - map which indicates if a given condition supports starts-with policy operator
    36  var startsWithConds = map[string]bool{
    37  	"$acl":                     true,
    38  	"$bucket":                  false,
    39  	"$cache-control":           true,
    40  	"$content-type":            true,
    41  	"$content-disposition":     true,
    42  	"$content-encoding":        true,
    43  	"$expires":                 true,
    44  	"$key":                     true,
    45  	"$success_action_redirect": true,
    46  	"$redirect":                true,
    47  	"$success_action_status":   false,
    48  	"$x-amz-algorithm":         false,
    49  	"$x-amz-credential":        false,
    50  	"$x-amz-date":              false,
    51  }
    52  
    53  // Add policy conditionals.
    54  const (
    55  	policyCondEqual         = "eq"
    56  	policyCondStartsWith    = "starts-with"
    57  	policyCondContentLength = "content-length-range"
    58  )
    59  
    60  // toString - Safely convert interface to string without causing panic.
    61  func toString(val interface{}) string {
    62  	switch v := val.(type) {
    63  	case string:
    64  		return v
    65  	default:
    66  		return ""
    67  	}
    68  }
    69  
    70  // toLowerString - safely convert interface to lower string
    71  func toLowerString(val interface{}) string {
    72  	return strings.ToLower(toString(val))
    73  }
    74  
    75  // toInteger _ Safely convert interface to integer without causing panic.
    76  func toInteger(val interface{}) (int64, error) {
    77  	switch v := val.(type) {
    78  	case float64:
    79  		return int64(v), nil
    80  	case int64:
    81  		return v, nil
    82  	case int:
    83  		return int64(v), nil
    84  	case string:
    85  		i, err := strconv.Atoi(v)
    86  		return int64(i), err
    87  	default:
    88  		return 0, errors.New("Invalid number format")
    89  	}
    90  }
    91  
    92  // isString - Safely check if val is of type string without causing panic.
    93  func isString(val interface{}) bool {
    94  	_, ok := val.(string)
    95  	return ok
    96  }
    97  
    98  // ContentLengthRange - policy content-length-range field.
    99  type contentLengthRange struct {
   100  	Min   int64
   101  	Max   int64
   102  	Valid bool // If content-length-range was part of policy
   103  }
   104  
   105  // PostPolicyForm provides strict static type conversion and validation for Amazon S3's POST policy JSON string.
   106  type PostPolicyForm struct {
   107  	Expiration time.Time // Expiration date and time of the POST policy.
   108  	Conditions struct {  // Conditional policy structure.
   109  		Policies []struct {
   110  			Operator string
   111  			Key      string
   112  			Value    string
   113  		}
   114  		ContentLengthRange contentLengthRange
   115  	}
   116  }
   117  
   118  // implemented to ensure that duplicate keys in JSON
   119  // are merged together into a single JSON key, also
   120  // to remove any extraneous JSON bodies.
   121  //
   122  // Go stdlib doesn't support parsing JSON with duplicate
   123  // keys, so we need to use this technique to merge the
   124  // keys.
   125  func sanitizePolicy(r io.Reader) (io.Reader, error) {
   126  	var buf bytes.Buffer
   127  	e := json.NewEncoder(&buf)
   128  	d := jstream.NewDecoder(r, 0).ObjectAsKVS()
   129  	sset := set.NewStringSet()
   130  	for mv := range d.Stream() {
   131  		var kvs jstream.KVS
   132  		if mv.ValueType == jstream.Object {
   133  			// This is a JSON object type (that preserves key order)
   134  			kvs = mv.Value.(jstream.KVS)
   135  			for _, kv := range kvs {
   136  				if sset.Contains(kv.Key) {
   137  					// Reject duplicate conditions or expiration.
   138  					return nil, fmt.Errorf("input policy has multiple %s, please fix your client code", kv.Key)
   139  				}
   140  				sset.Add(kv.Key)
   141  			}
   142  			e.Encode(kvs)
   143  		}
   144  	}
   145  	return &buf, d.Err()
   146  }
   147  
   148  // parsePostPolicyForm - Parse JSON policy string into typed PostPolicyForm structure.
   149  func parsePostPolicyForm(r io.Reader) (PostPolicyForm, error) {
   150  	reader, err := sanitizePolicy(r)
   151  	if err != nil {
   152  		return PostPolicyForm{}, err
   153  	}
   154  
   155  	d := json.NewDecoder(reader)
   156  
   157  	// Convert po into interfaces and
   158  	// perform strict type conversion using reflection.
   159  	var rawPolicy struct {
   160  		Expiration string        `json:"expiration"`
   161  		Conditions []interface{} `json:"conditions"`
   162  	}
   163  
   164  	d.DisallowUnknownFields()
   165  	if err := d.Decode(&rawPolicy); err != nil {
   166  		return PostPolicyForm{}, err
   167  	}
   168  
   169  	parsedPolicy := PostPolicyForm{}
   170  
   171  	// Parse expiry time.
   172  	parsedPolicy.Expiration, err = time.Parse(time.RFC3339Nano, rawPolicy.Expiration)
   173  	if err != nil {
   174  		return PostPolicyForm{}, err
   175  	}
   176  
   177  	// Parse conditions.
   178  	for _, val := range rawPolicy.Conditions {
   179  		switch condt := val.(type) {
   180  		case map[string]interface{}: // Handle key:value map types.
   181  			for k, v := range condt {
   182  				if !isString(v) { // Pre-check value type.
   183  					// All values must be of type string.
   184  					return parsedPolicy, fmt.Errorf("Unknown type %s of conditional field value %s found in POST policy form", reflect.TypeOf(condt).String(), condt)
   185  				}
   186  				// {"acl": "public-read" } is an alternate way to indicate - [ "eq", "$acl", "public-read" ]
   187  				// In this case we will just collapse this into "eq" for all use cases.
   188  				parsedPolicy.Conditions.Policies = append(parsedPolicy.Conditions.Policies, struct {
   189  					Operator string
   190  					Key      string
   191  					Value    string
   192  				}{
   193  					policyCondEqual, "$" + strings.ToLower(k), toString(v),
   194  				})
   195  			}
   196  		case []interface{}: // Handle array types.
   197  			if len(condt) != 3 { // Return error if we have insufficient elements.
   198  				return parsedPolicy, fmt.Errorf("Malformed conditional fields %s of type %s found in POST policy form", condt, reflect.TypeOf(condt).String())
   199  			}
   200  			switch toLowerString(condt[0]) {
   201  			case policyCondEqual, policyCondStartsWith:
   202  				for _, v := range condt { // Pre-check all values for type.
   203  					if !isString(v) {
   204  						// All values must be of type string.
   205  						return parsedPolicy, fmt.Errorf("Unknown type %s of conditional field value %s found in POST policy form", reflect.TypeOf(condt).String(), condt)
   206  					}
   207  				}
   208  				operator, matchType, value := toLowerString(condt[0]), toLowerString(condt[1]), toString(condt[2])
   209  				if !strings.HasPrefix(matchType, "$") {
   210  					return parsedPolicy, fmt.Errorf("Invalid according to Policy: Policy Condition failed: [%s, %s, %s]", operator, matchType, value)
   211  				}
   212  				parsedPolicy.Conditions.Policies = append(parsedPolicy.Conditions.Policies, struct {
   213  					Operator string
   214  					Key      string
   215  					Value    string
   216  				}{
   217  					operator, matchType, value,
   218  				})
   219  			case policyCondContentLength:
   220  				min, err := toInteger(condt[1])
   221  				if err != nil {
   222  					return parsedPolicy, err
   223  				}
   224  
   225  				max, err := toInteger(condt[2])
   226  				if err != nil {
   227  					return parsedPolicy, err
   228  				}
   229  
   230  				parsedPolicy.Conditions.ContentLengthRange = contentLengthRange{
   231  					Min:   min,
   232  					Max:   max,
   233  					Valid: true,
   234  				}
   235  			default:
   236  				// Condition should be valid.
   237  				return parsedPolicy, fmt.Errorf("Unknown type %s of conditional field value %s found in POST policy form",
   238  					reflect.TypeOf(condt).String(), condt)
   239  			}
   240  		default:
   241  			return parsedPolicy, fmt.Errorf("Unknown field %s of type %s found in POST policy form",
   242  				condt, reflect.TypeOf(condt).String())
   243  		}
   244  	}
   245  	return parsedPolicy, nil
   246  }
   247  
   248  // checkPolicyCond returns a boolean to indicate if a condition is satisified according
   249  // to the passed operator
   250  func checkPolicyCond(op string, input1, input2 string) bool {
   251  	switch op {
   252  	case policyCondEqual:
   253  		return input1 == input2
   254  	case policyCondStartsWith:
   255  		return strings.HasPrefix(input1, input2)
   256  	}
   257  	return false
   258  }
   259  
   260  // checkPostPolicy - apply policy conditions and validate input values.
   261  // (http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html)
   262  func checkPostPolicy(formValues http.Header, postPolicyForm PostPolicyForm) error {
   263  	// Check if policy document expiry date is still not reached
   264  	if !postPolicyForm.Expiration.After(UTCNow()) {
   265  		return fmt.Errorf("Invalid according to Policy: Policy expired")
   266  	}
   267  	// map to store the metadata
   268  	metaMap := make(map[string]string)
   269  	for _, policy := range postPolicyForm.Conditions.Policies {
   270  		if strings.HasPrefix(policy.Key, "$x-amz-meta-") {
   271  			formCanonicalName := http.CanonicalHeaderKey(strings.TrimPrefix(policy.Key, "$"))
   272  			metaMap[formCanonicalName] = policy.Value
   273  		}
   274  	}
   275  	// Check if any extra metadata field is passed as input
   276  	for key := range formValues {
   277  		if strings.HasPrefix(key, "X-Amz-Meta-") {
   278  			if _, ok := metaMap[key]; !ok {
   279  				return fmt.Errorf("Invalid according to Policy: Extra input fields: %s", key)
   280  			}
   281  		}
   282  	}
   283  
   284  	// Flag to indicate if all policies conditions are satisfied
   285  	var condPassed bool
   286  
   287  	// Iterate over policy conditions and check them against received form fields
   288  	for _, policy := range postPolicyForm.Conditions.Policies {
   289  		// Form fields names are in canonical format, convert conditions names
   290  		// to canonical for simplification purpose, so `$key` will become `Key`
   291  		formCanonicalName := http.CanonicalHeaderKey(strings.TrimPrefix(policy.Key, "$"))
   292  		// Operator for the current policy condition
   293  		op := policy.Operator
   294  		// If the current policy condition is known
   295  		if startsWithSupported, condFound := startsWithConds[policy.Key]; condFound {
   296  			// Check if the current condition supports starts-with operator
   297  			if op == policyCondStartsWith && !startsWithSupported {
   298  				return fmt.Errorf("Invalid according to Policy: Policy Condition failed")
   299  			}
   300  			// Check if current policy condition is satisfied
   301  			condPassed = checkPolicyCond(op, formValues.Get(formCanonicalName), policy.Value)
   302  			if !condPassed {
   303  				return fmt.Errorf("Invalid according to Policy: Policy Condition failed")
   304  			}
   305  		} else {
   306  			// This covers all conditions X-Amz-Meta-* and X-Amz-*
   307  			if strings.HasPrefix(policy.Key, "$x-amz-meta-") || strings.HasPrefix(policy.Key, "$x-amz-") {
   308  				// Check if policy condition is satisfied
   309  				condPassed = checkPolicyCond(op, formValues.Get(formCanonicalName), policy.Value)
   310  				if !condPassed {
   311  					return fmt.Errorf("Invalid according to Policy: Policy Condition failed: [%s, %s, %s]", op, policy.Key, policy.Value)
   312  				}
   313  			}
   314  		}
   315  	}
   316  
   317  	return nil
   318  }