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 }