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/>.
2016-09-13 22:00:01 -04:00
package cmd
import (
"bytes"
2018-03-15 16:27:16 -04:00
"context"
2016-09-13 22:00:01 -04:00
"encoding/base64"
"fmt"
2022-09-19 14:05:16 -04:00
"io"
2016-09-13 22:00:01 -04:00
"mime/multipart"
"net/http"
"net/http/httptest"
2017-03-13 17:41:13 -04:00
"net/url"
2023-03-20 04:06:45 -04:00
"strings"
2016-09-13 22:00:01 -04:00
"testing"
"time"
2016-11-22 21:18:22 -05:00
2021-10-28 10:35:28 -04:00
"github.com/dustin/go-humanize"
2016-09-13 22:00:01 -04:00
)
const (
2020-05-25 19:51:32 -04:00
iso8601DateFormat = "20060102T150405Z"
2016-09-13 22:00:01 -04:00
)
2016-11-21 07:15:26 -05:00
func newPostPolicyBytesV4WithContentRange ( credential , bucketName , objectKey string , expiration time . Time ) [ ] byte {
2017-03-18 14:28:41 -04:00
t := UTCNow ( )
2016-11-21 07:15:26 -05:00
// Add the expiration date.
2020-05-25 19:51:32 -04:00
expirationStr := fmt . Sprintf ( ` "expiration": "%s" ` , expiration . Format ( iso8601TimeFormat ) )
2016-11-21 07:15:26 -05:00
// Add the bucket condition, only accept buckets equal to the one passed.
bucketConditionStr := fmt . Sprintf ( ` ["eq", "$bucket", "%s"] ` , bucketName )
// Add the key condition, only accept keys equal to the one passed.
2016-12-04 15:23:19 -05:00
keyConditionStr := fmt . Sprintf ( ` ["eq", "$key", "%s/upload.txt"] ` , objectKey )
2016-11-21 07:15:26 -05:00
// Add content length condition, only accept content sizes of a given length.
contentLengthCondStr := ` ["content-length-range", 1024, 1048576] `
// Add the algorithm condition, only accept AWS SignV4 Sha256.
algorithmConditionStr := ` ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"] `
// Add the date condition, only accept the current date.
dateConditionStr := fmt . Sprintf ( ` ["eq", "$x-amz-date", "%s"] ` , t . Format ( iso8601DateFormat ) )
// Add the credential string, only accept the credential passed.
credentialConditionStr := fmt . Sprintf ( ` ["eq", "$x-amz-credential", "%s"] ` , credential )
2019-04-02 02:45:32 -04:00
// Add the meta-uuid string, set to 1234
uuidConditionStr := fmt . Sprintf ( ` ["eq", "$x-amz-meta-uuid", "%s"] ` , "1234" )
2016-11-21 07:15:26 -05:00
// Combine all conditions into one string.
2019-04-02 02:45:32 -04:00
conditionStr := fmt . Sprintf ( ` "conditions":[%s, %s, %s, %s, %s, %s, %s] ` , bucketConditionStr ,
keyConditionStr , contentLengthCondStr , algorithmConditionStr , dateConditionStr , credentialConditionStr , uuidConditionStr )
2016-11-21 07:15:26 -05:00
retStr := "{"
retStr = retStr + expirationStr + ","
2021-11-16 12:28:29 -05:00
retStr += conditionStr
retStr += "}"
2016-11-21 07:15:26 -05:00
return [ ] byte ( retStr )
}
2016-10-22 11:57:12 -04:00
// newPostPolicyBytesV4 - creates a bare bones postpolicy string with key and bucket matches.
func newPostPolicyBytesV4 ( credential , bucketName , objectKey string , expiration time . Time ) [ ] byte {
2017-03-18 14:28:41 -04:00
t := UTCNow ( )
2016-09-13 22:00:01 -04:00
// Add the expiration date.
2020-05-25 19:51:32 -04:00
expirationStr := fmt . Sprintf ( ` "expiration": "%s" ` , expiration . Format ( iso8601TimeFormat ) )
2016-09-13 22:00:01 -04:00
// Add the bucket condition, only accept buckets equal to the one passed.
bucketConditionStr := fmt . Sprintf ( ` ["eq", "$bucket", "%s"] ` , bucketName )
// Add the key condition, only accept keys equal to the one passed.
2016-12-04 15:23:19 -05:00
keyConditionStr := fmt . Sprintf ( ` ["eq", "$key", "%s/upload.txt"] ` , objectKey )
2016-09-13 22:00:01 -04:00
// Add the algorithm condition, only accept AWS SignV4 Sha256.
algorithmConditionStr := ` ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"] `
// Add the date condition, only accept the current date.
dateConditionStr := fmt . Sprintf ( ` ["eq", "$x-amz-date", "%s"] ` , t . Format ( iso8601DateFormat ) )
// Add the credential string, only accept the credential passed.
credentialConditionStr := fmt . Sprintf ( ` ["eq", "$x-amz-credential", "%s"] ` , credential )
2019-04-02 02:45:32 -04:00
// Add the meta-uuid string, set to 1234
uuidConditionStr := fmt . Sprintf ( ` ["eq", "$x-amz-meta-uuid", "%s"] ` , "1234" )
2016-09-13 22:00:01 -04:00
// Combine all conditions into one string.
2019-04-02 02:45:32 -04:00
conditionStr := fmt . Sprintf ( ` "conditions":[%s, %s, %s, %s, %s, %s] ` , bucketConditionStr , keyConditionStr , algorithmConditionStr , dateConditionStr , credentialConditionStr , uuidConditionStr )
2016-09-13 22:00:01 -04:00
retStr := "{"
retStr = retStr + expirationStr + ","
2021-11-16 12:28:29 -05:00
retStr += conditionStr
retStr += "}"
2016-09-13 22:00:01 -04:00
return [ ] byte ( retStr )
}
2016-10-22 11:57:12 -04:00
// newPostPolicyBytesV2 - creates a bare bones postpolicy string with key and bucket matches.
func newPostPolicyBytesV2 ( bucketName , objectKey string , expiration time . Time ) [ ] byte {
// Add the expiration date.
2020-05-25 19:51:32 -04:00
expirationStr := fmt . Sprintf ( ` "expiration": "%s" ` , expiration . Format ( iso8601TimeFormat ) )
2016-10-22 11:57:12 -04:00
// Add the bucket condition, only accept buckets equal to the one passed.
bucketConditionStr := fmt . Sprintf ( ` ["eq", "$bucket", "%s"] ` , bucketName )
// Add the key condition, only accept keys equal to the one passed.
2016-12-04 15:23:19 -05:00
keyConditionStr := fmt . Sprintf ( ` ["starts-with", "$key", "%s/upload.txt"] ` , objectKey )
2016-10-22 11:57:12 -04:00
// Combine all conditions into one string.
conditionStr := fmt . Sprintf ( ` "conditions":[%s, %s] ` , bucketConditionStr , keyConditionStr )
retStr := "{"
retStr = retStr + expirationStr + ","
2021-11-16 12:28:29 -05:00
retStr += conditionStr
retStr += "}"
2016-10-22 11:57:12 -04:00
return [ ] byte ( retStr )
}
2023-03-20 04:06:45 -04:00
// Wrapper
func TestPostPolicyReservedBucketExploit ( t * testing . T ) {
ExecObjectLayerTestWithDirs ( t , testPostPolicyReservedBucketExploit )
}
// testPostPolicyReservedBucketExploit is a test for the exploit fixed in PR
// #16849
func testPostPolicyReservedBucketExploit ( obj ObjectLayer , instanceType string , dirs [ ] string , t TestErrHandler ) {
if err := newTestConfig ( globalMinioDefaultRegion , obj ) ; err != nil {
t . Fatalf ( "Initializing config.json failed" )
}
// Register the API end points with Erasure/FS object layer.
apiRouter := initTestAPIEndPoints ( obj , [ ] string { "PostPolicy" } )
credentials := globalActiveCred
bucketName := minioMetaBucket
objectName := "config/x"
// This exploit needs browser to be enabled.
if ! globalBrowserEnabled {
globalBrowserEnabled = true
defer func ( ) { globalBrowserEnabled = false } ( )
}
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
req , perr := newPostRequestV4 ( "" , bucketName , objectName , [ ] byte ( "pwned" ) , credentials . AccessKey , credentials . SecretKey )
if perr != nil {
t . Fatalf ( "Test %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v" , instanceType , perr )
}
contentTypeHdr := req . Header . Get ( "Content-Type" )
contentTypeHdr = strings . Replace ( contentTypeHdr , "multipart/form-data" , "multipart/form-datA" , 1 )
req . Header . Set ( "Content-Type" , contentTypeHdr )
req . Header . Set ( "User-Agent" , "Mozilla" )
2024-01-18 02:03:17 -05:00
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
2023-03-20 04:06:45 -04:00
// Call the ServeHTTP to execute the handler.
apiRouter . ServeHTTP ( rec , req )
ctx , cancel := context . WithCancel ( GlobalContext )
defer cancel ( )
// Now check if we actually wrote to backend (regardless of the response
// returned by the server).
z := obj . ( * erasureServerPools )
xl := z . serverPools [ 0 ] . sets [ 0 ]
erasureDisks := xl . getDisks ( )
2024-01-30 15:43:25 -05:00
parts , errs := readAllFileInfo ( ctx , erasureDisks , "" , bucketName , objectName + "/upload.txt" , "" , false , false )
2023-03-20 04:06:45 -04:00
for i := range parts {
if errs [ i ] == nil {
if parts [ i ] . Name == objectName + "/upload.txt" {
t . Errorf ( "Test %s: Failed to stop post policy handler from writing to minioMetaBucket" , instanceType )
}
}
}
}
2020-06-12 23:04:01 -04:00
// Wrapper for calling TestPostPolicyBucketHandler tests for both Erasure multiple disks and single node setup.
2016-12-04 15:23:19 -05:00
func TestPostPolicyBucketHandler ( t * testing . T ) {
ExecObjectLayerTest ( t , testPostPolicyBucketHandler )
2016-09-13 22:00:01 -04:00
}
2016-12-04 15:23:19 -05:00
// testPostPolicyBucketHandler - Tests validate post policy handler uploading objects.
func testPostPolicyBucketHandler ( obj ObjectLayer , instanceType string , t TestErrHandler ) {
2018-08-15 00:41:47 -04:00
if err := newTestConfig ( globalMinioDefaultRegion , obj ) ; err != nil {
2016-10-05 15:48:07 -04:00
t . Fatalf ( "Initializing config.json failed" )
}
2016-09-13 22:00:01 -04:00
// get random bucket name.
bucketName := getRandomBucketName ( )
2018-09-10 12:42:43 -04:00
var opts ObjectOptions
2020-06-12 23:04:01 -04:00
// Register the API end points with Erasure/FS object layer.
2016-09-13 22:00:01 -04:00
apiRouter := initTestAPIEndPoints ( obj , [ ] string { "PostPolicy" } )
2019-10-23 01:59:13 -04:00
credentials := globalActiveCred
2016-09-15 02:53:42 -04:00
2017-03-18 14:28:41 -04:00
curTime := UTCNow ( )
2016-12-04 15:23:19 -05:00
curTimePlus5Min := curTime . Add ( time . Minute * 5 )
2016-09-13 22:00:01 -04:00
// bucketnames[0].
// objectNames[0].
// uploadIds [0].
// Create bucket before initiating NewMultipartUpload.
2022-12-23 10:46:00 -05:00
err := obj . MakeBucket ( context . Background ( ) , bucketName , MakeBucketOptions { } )
2016-09-13 22:00:01 -04:00
if err != nil {
// Failed to create newbucket, abort.
t . Fatalf ( "%s : %s" , instanceType , err . Error ( ) )
}
2016-10-22 11:57:12 -04:00
// Test cases for signature-V2.
testCasesV2 := [ ] struct {
expectedStatus int
secretKey string
2024-05-06 06:52:41 -04:00
formData map [ string ] string
2016-10-22 11:57:12 -04:00
} {
2024-05-06 06:52:41 -04:00
{ http . StatusForbidden , credentials . SecretKey , map [ string ] string { "AWSAccessKeyId" : "invalidaccesskey" } } ,
{ http . StatusForbidden , "invalidsecretkey" , map [ string ] string { "AWSAccessKeyId" : credentials . AccessKey } } ,
{ http . StatusNoContent , credentials . SecretKey , map [ string ] string { "AWSAccessKeyId" : credentials . AccessKey } } ,
{ http . StatusForbidden , credentials . SecretKey , map [ string ] string { "Awsaccesskeyid" : "invalidaccesskey" } } ,
{ http . StatusForbidden , "invalidsecretkey" , map [ string ] string { "Awsaccesskeyid" : credentials . AccessKey } } ,
{ http . StatusNoContent , credentials . SecretKey , map [ string ] string { "Awsaccesskeyid" : credentials . AccessKey } } ,
// Forbidden with key not in policy.conditions for signed requests V2.
{ http . StatusForbidden , credentials . SecretKey , map [ string ] string { "Awsaccesskeyid" : credentials . AccessKey , "AnotherKey" : "AnotherContent" } } ,
2016-10-22 11:57:12 -04:00
}
for i , test := range testCasesV2 {
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
2024-05-06 06:52:41 -04:00
req , perr := newPostRequestV2 ( "" , bucketName , "testobject" , test . secretKey , test . formData )
2016-10-22 11:57:12 -04:00
if perr != nil {
t . Fatalf ( "Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v" , i + 1 , instanceType , perr )
}
2024-01-18 02:03:17 -05:00
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
2016-10-22 11:57:12 -04:00
// Call the ServeHTTP to execute the handler.
apiRouter . ServeHTTP ( rec , req )
if rec . Code != test . expectedStatus {
2024-05-22 19:07:14 -04:00
t . Fatalf ( "Test %d: %s: Expected the response status to be `%d`, but instead found `%d`, Resp: %s" , i + 1 , instanceType , test . expectedStatus , rec . Code , rec . Body )
2016-10-22 11:57:12 -04:00
}
}
// Test cases for signature-V4.
testCasesV4 := [ ] struct {
2016-09-13 22:00:01 -04:00
objectName string
data [ ] byte
2016-12-19 19:14:04 -05:00
expectedHeaders map [ string ] string
2016-09-13 22:00:01 -04:00
expectedRespStatus int
2016-09-15 02:53:42 -04:00
accessKey string
secretKey string
malformedBody bool
2016-09-13 22:00:01 -04:00
} {
// Success case.
{
objectName : "test" ,
data : [ ] byte ( "Hello, World" ) ,
expectedRespStatus : http . StatusNoContent ,
2016-12-19 19:14:04 -05:00
expectedHeaders : map [ string ] string { "X-Amz-Meta-Uuid" : "1234" } ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-09-15 02:53:42 -04:00
malformedBody : false ,
2016-09-13 22:00:01 -04:00
} ,
2016-09-15 02:53:42 -04:00
// Bad case invalid request.
2016-09-13 22:00:01 -04:00
{
objectName : "test" ,
data : [ ] byte ( "Hello, World" ) ,
2020-08-24 01:06:22 -04:00
expectedRespStatus : http . StatusForbidden ,
2016-09-15 02:53:42 -04:00
accessKey : "" ,
secretKey : "" ,
malformedBody : false ,
} ,
// Bad case malformed input.
{
objectName : "test" ,
data : [ ] byte ( "Hello, World" ) ,
expectedRespStatus : http . StatusBadRequest ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2016-09-15 02:53:42 -04:00
malformedBody : true ,
2016-09-13 22:00:01 -04:00
} ,
}
2016-10-22 11:57:12 -04:00
for i , testCase := range testCasesV4 {
2016-09-13 22:00:01 -04:00
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
2016-10-22 11:57:12 -04:00
req , perr := newPostRequestV4 ( "" , bucketName , testCase . objectName , testCase . data , testCase . accessKey , testCase . secretKey )
2016-09-13 22:00:01 -04:00
if perr != nil {
t . Fatalf ( "Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v" , i + 1 , instanceType , perr )
}
2016-09-15 02:53:42 -04:00
if testCase . malformedBody {
// Change the request body.
2022-09-19 14:05:16 -04:00
req . Body = io . NopCloser ( bytes . NewReader ( [ ] byte ( "Hello," ) ) )
2016-09-15 02:53:42 -04:00
}
2024-01-18 02:03:17 -05:00
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
2016-09-13 22:00:01 -04:00
// Call the ServeHTTP to execute the handler.
apiRouter . ServeHTTP ( rec , req )
if rec . Code != testCase . expectedRespStatus {
t . Errorf ( "Test %d: %s: Expected the response status to be `%d`, but instead found `%d`" , i + 1 , instanceType , testCase . expectedRespStatus , rec . Code )
}
2016-12-19 19:14:04 -05:00
// When the operation is successful, check if sending metadata is successful too
if rec . Code == http . StatusNoContent {
2018-09-10 12:42:43 -04:00
objInfo , err := obj . GetObjectInfo ( context . Background ( ) , bucketName , testCase . objectName + "/upload.txt" , opts )
2016-12-19 19:14:04 -05:00
if err != nil {
t . Error ( "Unexpected error: " , err )
}
for k , v := range testCase . expectedHeaders {
if objInfo . UserDefined [ k ] != v {
t . Errorf ( "Expected to have header %s with value %s, but found value `%s` instead" , k , v , objInfo . UserDefined [ k ] )
}
}
}
2016-09-13 22:00:01 -04:00
}
2016-11-21 07:15:26 -05:00
2017-05-15 21:17:02 -04:00
region := "us-east-1"
2016-12-04 15:23:19 -05:00
// Test cases for signature-V4.
testCasesV4BadData := [ ] struct {
objectName string
data [ ] byte
expectedRespStatus int
accessKey string
secretKey string
dates [ ] interface { }
policy string
2023-10-31 00:06:32 -04:00
noFilename bool
2016-12-04 15:23:19 -05:00
corruptedBase64 bool
corruptedMultipart bool
} {
// Success case.
{
objectName : "test" ,
data : [ ] byte ( "Hello, World" ) ,
expectedRespStatus : http . StatusNoContent ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2020-05-25 19:51:32 -04:00
dates : [ ] interface { } { curTimePlus5Min . Format ( iso8601TimeFormat ) , curTime . Format ( iso8601DateFormat ) , curTime . Format ( yyyymmdd ) } ,
2019-04-02 02:45:32 -04:00
policy : ` { "expiration": "%s","conditions":[["eq", "$bucket", " ` + bucketName + ` "], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", " ` + credentials . AccessKey + ` /%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"]]} ` ,
2021-10-28 10:35:28 -04:00
} ,
2023-10-31 00:06:32 -04:00
// Success case, no multipart filename.
{
objectName : "test" ,
data : [ ] byte ( "Hello, World" ) ,
expectedRespStatus : http . StatusNoContent ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
dates : [ ] interface { } { curTimePlus5Min . Format ( iso8601TimeFormat ) , curTime . Format ( iso8601DateFormat ) , curTime . Format ( yyyymmdd ) } ,
policy : ` { "expiration": "%s","conditions":[["eq", "$bucket", " ` + bucketName + ` "], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", " ` + credentials . AccessKey + ` /%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"]]} ` ,
noFilename : true ,
} ,
2021-10-28 10:35:28 -04:00
// Success case, big body.
{
objectName : "test" ,
data : bytes . Repeat ( [ ] byte ( "a" ) , 10 << 20 ) ,
expectedRespStatus : http . StatusNoContent ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
dates : [ ] interface { } { curTimePlus5Min . Format ( iso8601TimeFormat ) , curTime . Format ( iso8601DateFormat ) , curTime . Format ( yyyymmdd ) } ,
policy : ` { "expiration": "%s","conditions":[["eq", "$bucket", " ` + bucketName + ` "], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", " ` + credentials . AccessKey + ` /%s/us-east-1/s3/aws4_request"],["eq", "$x-amz-meta-uuid", "1234"]]} ` ,
2016-12-04 15:23:19 -05:00
} ,
// Corrupted Base 64 result
{
objectName : "test" ,
data : [ ] byte ( "Hello, World" ) ,
expectedRespStatus : http . StatusBadRequest ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2020-05-25 19:51:32 -04:00
dates : [ ] interface { } { curTimePlus5Min . Format ( iso8601TimeFormat ) , curTime . Format ( iso8601DateFormat ) , curTime . Format ( yyyymmdd ) } ,
2016-12-26 13:21:23 -05:00
policy : ` { "expiration": "%s","conditions":[["eq", "$bucket", " ` + bucketName + ` "], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", " ` + credentials . AccessKey + ` /%s/us-east-1/s3/aws4_request"]]} ` ,
2016-12-04 15:23:19 -05:00
corruptedBase64 : true ,
} ,
// Corrupted Multipart body
{
objectName : "test" ,
data : [ ] byte ( "Hello, World" ) ,
expectedRespStatus : http . StatusBadRequest ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2020-05-25 19:51:32 -04:00
dates : [ ] interface { } { curTimePlus5Min . Format ( iso8601TimeFormat ) , curTime . Format ( iso8601DateFormat ) , curTime . Format ( yyyymmdd ) } ,
2016-12-26 13:21:23 -05:00
policy : ` { "expiration": "%s","conditions":[["eq", "$bucket", " ` + bucketName + ` "], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", " ` + credentials . AccessKey + ` /%s/us-east-1/s3/aws4_request"]]} ` ,
2016-12-04 15:23:19 -05:00
corruptedMultipart : true ,
} ,
// Bad case invalid request.
{
objectName : "test" ,
data : [ ] byte ( "Hello, World" ) ,
2020-08-24 01:06:22 -04:00
expectedRespStatus : http . StatusForbidden ,
2016-12-04 15:23:19 -05:00
accessKey : "" ,
secretKey : "" ,
dates : [ ] interface { } { } ,
policy : ` ` ,
} ,
// Expired document
{
objectName : "test" ,
data : [ ] byte ( "Hello, World" ) ,
2019-03-05 15:10:47 -05:00
expectedRespStatus : http . StatusForbidden ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2020-05-25 19:51:32 -04:00
dates : [ ] interface { } { curTime . Add ( - 1 * time . Minute * 5 ) . Format ( iso8601TimeFormat ) , curTime . Format ( iso8601DateFormat ) , curTime . Format ( yyyymmdd ) } ,
2016-12-26 13:21:23 -05:00
policy : ` { "expiration": "%s","conditions":[["eq", "$bucket", " ` + bucketName + ` "], ["starts-with", "$key", "test/"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", " ` + credentials . AccessKey + ` /%s/us-east-1/s3/aws4_request"]]} ` ,
2016-12-04 15:23:19 -05:00
} ,
// Corrupted policy document
{
objectName : "test" ,
data : [ ] byte ( "Hello, World" ) ,
2019-09-22 17:20:49 -04:00
expectedRespStatus : http . StatusForbidden ,
2016-12-26 13:21:23 -05:00
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
2020-05-25 19:51:32 -04:00
dates : [ ] interface { } { curTimePlus5Min . Format ( iso8601TimeFormat ) , curTime . Format ( iso8601DateFormat ) , curTime . Format ( yyyymmdd ) } ,
2016-12-04 15:23:19 -05:00
policy : ` { "3/aws4_request"]]} ` ,
} ,
}
for i , testCase := range testCasesV4BadData {
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
testCase . policy = fmt . Sprintf ( testCase . policy , testCase . dates ... )
2016-12-20 12:32:17 -05:00
2016-12-04 15:23:19 -05:00
req , perr := newPostRequestV4Generic ( "" , bucketName , testCase . objectName , testCase . data , testCase . accessKey ,
2023-10-31 00:06:32 -04:00
testCase . secretKey , region , curTime , [ ] byte ( testCase . policy ) , nil , testCase . noFilename , testCase . corruptedBase64 , testCase . corruptedMultipart )
2016-12-04 15:23:19 -05:00
if perr != nil {
t . Fatalf ( "Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v" , i + 1 , instanceType , perr )
}
2024-01-18 02:03:17 -05:00
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
2016-12-04 15:23:19 -05:00
// Call the ServeHTTP to execute the handler.
apiRouter . ServeHTTP ( rec , req )
if rec . Code != testCase . expectedRespStatus {
t . Errorf ( "Test %d: %s: Expected the response status to be `%d`, but instead found `%d`" , i + 1 , instanceType , testCase . expectedRespStatus , rec . Code )
}
}
2016-11-21 07:15:26 -05:00
testCases2 := [ ] struct {
2017-02-02 13:45:00 -05:00
objectName string
data [ ] byte
expectedRespStatus int
accessKey string
secretKey string
malformedBody bool
ignoreContentLength bool
2016-11-21 07:15:26 -05:00
} {
// Success case.
{
2017-02-02 13:45:00 -05:00
objectName : "test" ,
data : bytes . Repeat ( [ ] byte ( "a" ) , 1025 ) ,
expectedRespStatus : http . StatusNoContent ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
malformedBody : false ,
ignoreContentLength : false ,
} ,
// Failed with Content-Length not specified.
{
objectName : "test" ,
data : bytes . Repeat ( [ ] byte ( "a" ) , 1025 ) ,
expectedRespStatus : http . StatusNoContent ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
malformedBody : false ,
ignoreContentLength : true ,
2016-11-21 07:15:26 -05:00
} ,
// Failed with entity too small.
{
2017-02-02 13:45:00 -05:00
objectName : "test" ,
data : bytes . Repeat ( [ ] byte ( "a" ) , 1023 ) ,
expectedRespStatus : http . StatusBadRequest ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
malformedBody : false ,
ignoreContentLength : false ,
2016-11-21 07:15:26 -05:00
} ,
// Failed with entity too large.
{
2017-02-02 13:45:00 -05:00
objectName : "test" ,
data : bytes . Repeat ( [ ] byte ( "a" ) , ( 1 * humanize . MiByte ) + 1 ) ,
expectedRespStatus : http . StatusBadRequest ,
accessKey : credentials . AccessKey ,
secretKey : credentials . SecretKey ,
malformedBody : false ,
ignoreContentLength : false ,
2016-11-21 07:15:26 -05:00
} ,
}
for i , testCase := range testCases2 {
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
2017-02-02 13:45:00 -05:00
var req * http . Request
var perr error
if testCase . ignoreContentLength {
req , perr = newPostRequestV4 ( "" , bucketName , testCase . objectName , testCase . data , testCase . accessKey , testCase . secretKey )
} else {
req , perr = newPostRequestV4WithContentLength ( "" , bucketName , testCase . objectName , testCase . data , testCase . accessKey , testCase . secretKey )
}
2016-11-21 07:15:26 -05:00
if perr != nil {
t . Fatalf ( "Test %d: %s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v" , i + 1 , instanceType , perr )
}
2024-01-18 02:03:17 -05:00
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
2016-11-21 07:15:26 -05:00
// Call the ServeHTTP to execute the handler.
apiRouter . ServeHTTP ( rec , req )
if rec . Code != testCase . expectedRespStatus {
t . Errorf ( "Test %d: %s: Expected the response status to be `%d`, but instead found `%d`" , i + 1 , instanceType , testCase . expectedRespStatus , rec . Code )
}
}
2016-09-13 22:00:01 -04:00
}
2020-06-12 23:04:01 -04:00
// Wrapper for calling TestPostPolicyBucketHandlerRedirect tests for both Erasure multiple disks and single node setup.
2016-12-20 12:32:17 -05:00
func TestPostPolicyBucketHandlerRedirect ( t * testing . T ) {
ExecObjectLayerTest ( t , testPostPolicyBucketHandlerRedirect )
}
// testPostPolicyBucketHandlerRedirect tests POST Object when success_action_redirect is specified
func testPostPolicyBucketHandlerRedirect ( obj ObjectLayer , instanceType string , t TestErrHandler ) {
2018-08-15 00:41:47 -04:00
if err := newTestConfig ( globalMinioDefaultRegion , obj ) ; err != nil {
2016-12-20 12:32:17 -05:00
t . Fatalf ( "Initializing config.json failed" )
}
// get random bucket name.
bucketName := getRandomBucketName ( )
// Key specified in Form data
keyName := "test/object"
2018-09-10 12:42:43 -04:00
var opts ObjectOptions
2016-12-20 12:32:17 -05:00
// The final name of the upload object
targetObj := keyName + "/upload.txt"
// The url of success_action_redirect field
2022-07-13 10:46:44 -04:00
redirectURL , err := url . Parse ( "http://www.google.com?query=value" )
2017-03-13 17:41:13 -04:00
if err != nil {
t . Fatal ( err )
}
2016-12-20 12:32:17 -05:00
2020-06-12 23:04:01 -04:00
// Register the API end points with Erasure/FS object layer.
2016-12-20 12:32:17 -05:00
apiRouter := initTestAPIEndPoints ( obj , [ ] string { "PostPolicy" } )
2019-10-23 01:59:13 -04:00
credentials := globalActiveCred
2016-12-20 12:32:17 -05:00
2017-03-18 14:28:41 -04:00
curTime := UTCNow ( )
2016-12-20 12:32:17 -05:00
curTimePlus5Min := curTime . Add ( time . Minute * 5 )
2022-12-23 10:46:00 -05:00
err = obj . MakeBucket ( context . Background ( ) , bucketName , MakeBucketOptions { } )
2016-12-20 12:32:17 -05:00
if err != nil {
// Failed to create newbucket, abort.
t . Fatalf ( "%s : %s" , instanceType , err . Error ( ) )
}
// initialize HTTP NewRecorder, this records any mutations to response writer inside the handler.
rec := httptest . NewRecorder ( )
2020-05-25 19:51:32 -04:00
dates := [ ] interface { } { curTimePlus5Min . Format ( iso8601TimeFormat ) , curTime . Format ( iso8601DateFormat ) , curTime . Format ( yyyymmdd ) }
2019-04-02 02:45:32 -04:00
policy := ` { "expiration": "%s","conditions":[["eq", "$bucket", " ` + bucketName + ` "], { "success_action_redirect":" ` + redirectURL . String ( ) + ` "},["starts-with", "$key", "test/"], ["eq", "$x-amz-meta-uuid", "1234"], ["eq", "$x-amz-algorithm", "AWS4-HMAC-SHA256"], ["eq", "$x-amz-date", "%s"], ["eq", "$x-amz-credential", " ` + credentials . AccessKey + ` /%s/us-east-1/s3/aws4_request"]]} `
2016-12-20 12:32:17 -05:00
// Generate the final policy document
policy = fmt . Sprintf ( policy , dates ... )
2017-05-15 21:17:02 -04:00
region := "us-east-1"
2016-12-20 12:32:17 -05:00
// Create a new POST request with success_action_redirect field specified
req , perr := newPostRequestV4Generic ( "" , bucketName , keyName , [ ] byte ( "objData" ) ,
2017-05-15 21:17:02 -04:00
credentials . AccessKey , credentials . SecretKey , region , curTime ,
2023-10-31 00:06:32 -04:00
[ ] byte ( policy ) , map [ string ] string { "success_action_redirect" : redirectURL . String ( ) } , false , false , false )
2016-12-20 12:32:17 -05:00
if perr != nil {
t . Fatalf ( "%s: Failed to create HTTP request for PostPolicyHandler: <ERROR> %v" , instanceType , perr )
}
2024-01-18 02:03:17 -05:00
// Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler.
2016-12-20 12:32:17 -05:00
// Call the ServeHTTP to execute the handler.
apiRouter . ServeHTTP ( rec , req )
// Check the status code, which must be 303 because success_action_redirect is specified
if rec . Code != http . StatusSeeOther {
t . Errorf ( "%s: Expected the response status to be `%d`, but instead found `%d`" , instanceType , http . StatusSeeOther , rec . Code )
}
// Get the uploaded object info
2018-09-10 12:42:43 -04:00
info , err := obj . GetObjectInfo ( context . Background ( ) , bucketName , targetObj , opts )
2016-12-20 12:32:17 -05:00
if err != nil {
t . Error ( "Unexpected error: " , err )
}
2022-07-15 18:04:48 -04:00
v := redirectURL . Query ( )
v . Add ( "bucket" , info . Bucket )
v . Add ( "key" , info . Name )
v . Add ( "etag" , "\"" + info . ETag + "\"" )
redirectURL . RawQuery = v . Encode ( )
2017-03-13 17:41:13 -04:00
expectedLocation := redirectURL . String ( )
2016-12-20 12:32:17 -05:00
// Check the new location url
2020-05-18 12:59:45 -04:00
if rec . Header ( ) . Get ( "Location" ) != expectedLocation {
t . Errorf ( "Unexpected location, expected = %s, found = `%s`" , rec . Header ( ) . Get ( "Location" ) , expectedLocation )
2016-12-20 12:32:17 -05:00
}
}
2016-09-13 22:00:01 -04:00
// postPresignSignatureV4 - presigned signature for PostPolicy requests.
func postPresignSignatureV4 ( policyBase64 string , t time . Time , secretAccessKey , location string ) string {
// Get signining key.
2019-02-27 20:46:55 -05:00
signingkey := getSigningKey ( secretAccessKey , t , location , "s3" )
2016-09-13 22:00:01 -04:00
// Calculate signature.
signature := getSignature ( signingkey , policyBase64 )
return signature
}
2024-05-06 06:52:41 -04:00
func newPostRequestV2 ( endPoint , bucketName , objectName string , secretKey string , formInputData map [ string ] string ) ( * http . Request , error ) {
2016-10-22 11:57:12 -04:00
// Expire the request five minutes from now.
2017-03-18 14:28:41 -04:00
expirationTime := UTCNow ( ) . Add ( time . Minute * 5 )
2016-10-22 11:57:12 -04:00
// Create a new post policy.
policy := newPostPolicyBytesV2 ( bucketName , objectName , expirationTime )
// Only need the encoding.
encodedPolicy := base64 . StdEncoding . EncodeToString ( policy )
// Presign with V4 signature based on the policy.
signature := calculateSignatureV2 ( encodedPolicy , secretKey )
formData := map [ string ] string {
2024-05-06 06:52:41 -04:00
"bucket" : bucketName ,
"key" : objectName + "/${filename}" ,
"policy" : encodedPolicy ,
"signature" : signature ,
}
for key , value := range formInputData {
formData [ key ] = value
2016-10-22 11:57:12 -04:00
}
// Create the multipart form.
var buf bytes . Buffer
w := multipart . NewWriter ( & buf )
// Set the normal formData
for k , v := range formData {
w . WriteField ( k , v )
}
// Set the File formData
2016-12-04 15:23:19 -05:00
writer , err := w . CreateFormFile ( "file" , "upload.txt" )
2016-10-22 11:57:12 -04:00
if err != nil {
// return nil, err
return nil , err
}
writer . Write ( [ ] byte ( "hello world" ) )
// Close before creating the new request.
w . Close ( )
// Set the body equal to the created policy.
reader := bytes . NewReader ( buf . Bytes ( ) )
2020-07-20 15:52:49 -04:00
req , err := http . NewRequest ( http . MethodPost , makeTestTargetURL ( endPoint , bucketName , "" , nil ) , reader )
2016-10-22 11:57:12 -04:00
if err != nil {
return nil , err
}
// Set form content-type.
req . Header . Set ( "Content-Type" , w . FormDataContentType ( ) )
return req , nil
}
2017-05-15 21:17:02 -04:00
func buildGenericPolicy ( t time . Time , accessKey , region , bucketName , objectName string , contentLengthRange bool ) [ ] byte {
2016-09-13 22:00:01 -04:00
// Expire the request five minutes from now.
expirationTime := t . Add ( time . Minute * 5 )
2016-12-04 15:23:19 -05:00
2017-05-15 21:17:02 -04:00
credStr := getCredentialString ( accessKey , region , t )
2016-09-13 22:00:01 -04:00
// Create a new post policy.
2016-10-22 11:57:12 -04:00
policy := newPostPolicyBytesV4 ( credStr , bucketName , objectName , expirationTime )
2016-11-21 07:15:26 -05:00
if contentLengthRange {
policy = newPostPolicyBytesV4WithContentRange ( credStr , bucketName , objectName , expirationTime )
}
2016-12-04 15:23:19 -05:00
return policy
}
2017-05-15 21:17:02 -04:00
func newPostRequestV4Generic ( endPoint , bucketName , objectName string , objData [ ] byte , accessKey , secretKey string , region string ,
2023-10-31 00:06:32 -04:00
t time . Time , policy [ ] byte , addFormData map [ string ] string , noFilename bool , corruptedB64 bool , corruptedMultipart bool ,
2022-04-13 15:00:11 -04:00
) ( * http . Request , error ) {
2016-12-04 15:23:19 -05:00
// Get the user credential.
2017-05-15 21:17:02 -04:00
credStr := getCredentialString ( accessKey , region , t )
2016-12-04 15:23:19 -05:00
2016-09-13 22:00:01 -04:00
// Only need the encoding.
encodedPolicy := base64 . StdEncoding . EncodeToString ( policy )
2016-12-04 15:23:19 -05:00
if corruptedB64 {
encodedPolicy = "%!~&" + encodedPolicy
}
2016-09-15 02:53:42 -04:00
// Presign with V4 signature based on the policy.
2017-05-15 21:17:02 -04:00
signature := postPresignSignatureV4 ( encodedPolicy , t , secretKey , region )
2016-09-15 02:53:42 -04:00
2023-10-31 00:06:32 -04:00
// If there is no filename on multipart, get the filename from the key.
key := objectName
if noFilename {
key += "/upload.txt"
} else {
key += "/${filename}"
}
2016-09-15 02:53:42 -04:00
formData := map [ string ] string {
"bucket" : bucketName ,
2023-10-31 00:06:32 -04:00
"key" : key ,
2016-09-15 02:53:42 -04:00
"x-amz-credential" : credStr ,
"policy" : encodedPolicy ,
"x-amz-signature" : signature ,
"x-amz-date" : t . Format ( iso8601DateFormat ) ,
"x-amz-algorithm" : "AWS4-HMAC-SHA256" ,
2016-12-19 19:14:04 -05:00
"x-amz-meta-uuid" : "1234" ,
"Content-Encoding" : "gzip" ,
2016-09-13 22:00:01 -04:00
}
2016-12-20 12:32:17 -05:00
// Add form data
for k , v := range addFormData {
formData [ k ] = v
}
2016-09-13 22:00:01 -04:00
// Create the multipart form.
var buf bytes . Buffer
w := multipart . NewWriter ( & buf )
// Set the normal formData
for k , v := range formData {
w . WriteField ( k , v )
}
2016-12-04 15:23:19 -05:00
// Set the File formData but don't if we want send an incomplete multipart request
if ! corruptedMultipart {
2023-10-31 00:06:32 -04:00
var writer io . Writer
var err error
if noFilename {
writer , err = w . CreateFormField ( "file" )
} else {
writer , err = w . CreateFormFile ( "file" , "upload.txt" )
}
2016-12-04 15:23:19 -05:00
if err != nil {
// return nil, err
return nil , err
}
writer . Write ( objData )
// Close before creating the new request.
w . Close ( )
2016-09-13 22:00:01 -04:00
}
// Set the body equal to the created policy.
reader := bytes . NewReader ( buf . Bytes ( ) )
2020-07-20 15:52:49 -04:00
req , err := http . NewRequest ( http . MethodPost , makeTestTargetURL ( endPoint , bucketName , "" , nil ) , reader )
2016-09-13 22:00:01 -04:00
if err != nil {
return nil , err
}
// Set form content-type.
req . Header . Set ( "Content-Type" , w . FormDataContentType ( ) )
return req , nil
}
2016-11-21 07:15:26 -05:00
func newPostRequestV4WithContentLength ( endPoint , bucketName , objectName string , objData [ ] byte , accessKey , secretKey string ) ( * http . Request , error ) {
2017-03-18 14:28:41 -04:00
t := UTCNow ( )
2017-05-15 21:17:02 -04:00
region := "us-east-1"
policy := buildGenericPolicy ( t , accessKey , region , bucketName , objectName , true )
2023-10-31 00:06:32 -04:00
return newPostRequestV4Generic ( endPoint , bucketName , objectName , objData , accessKey , secretKey , region , t , policy , nil , false , false , false )
2016-11-21 07:15:26 -05:00
}
func newPostRequestV4 ( endPoint , bucketName , objectName string , objData [ ] byte , accessKey , secretKey string ) ( * http . Request , error ) {
2017-03-18 14:28:41 -04:00
t := UTCNow ( )
2017-05-15 21:17:02 -04:00
region := "us-east-1"
policy := buildGenericPolicy ( t , accessKey , region , bucketName , objectName , false )
2023-10-31 00:06:32 -04:00
return newPostRequestV4Generic ( endPoint , bucketName , objectName , objData , accessKey , secretKey , region , t , policy , nil , false , false , false )
2016-11-21 07:15:26 -05:00
}