user1952811
user1952811

Reputation: 2468

Amazon S3 Content Type won't be set when doing a POST request

AccessDenied Invalid according to Policy: Extra input fields: content-type

When I have this

------WebKitFormBoundaryZIsnhgiAKpAVIsBT
Content-Disposition: form-data; name="acl"

public-read
------WebKitFormBoundaryZIsnhgiAKpAVIsBT
Content-Disposition: form-data; name="awsaccesskeyid"

my-access-key-id
------WebKitFormBoundaryZIsnhgiAKpAVIsBT
Content-Disposition: form-data; name="bucket"

my-bucket
------WebKitFormBoundaryZIsnhgiAKpAVIsBT
Content-Disposition: form-data; name="Content-Type"

image/png
------WebKitFormBoundaryZIsnhgiAKpAVIsBT
Content-Disposition: form-data; name="key"

images/anime_girl.png
------WebKitFormBoundaryZIsnhgiAKpAVIsBT
Content-Disposition: form-data; name="policy"

my-policy
------WebKitFormBoundaryZIsnhgiAKpAVIsBT
Content-Disposition: form-data; name="signature"

my-signature
------WebKitFormBoundaryZIsnhgiAKpAVIsBT
Content-Disposition: form-data; name="success_action_status"

201
------WebKitFormBoundaryZIsnhgiAKpAVIsBT
Content-Disposition: form-data; name="file"; filename="anime_girl.png"
Content-Type: image/png

However, if I completely omit the Content Type in the code below, the file gets uploaded but doesn't have the right content type.

        "acl" => "public-read",
        "awsaccesskeyid" => $s3->getAwsAccessKeyId(),
        "bucket" => $s3->getBucket(),
        "Content-Type" => $inputs['type'],
        "key" => "images/" . $inputs['name'],
        "policy" => $s3->getPolicy(true),
        "signature" => $s3->getSignedPolicy(),
        "success_action_status" => "201"

I've also tried adding the content type into the policy and I get this error

AccessDenied Invalid according to Policy: Policy Condition failed: ["starts-with", "Content-Type", "image/png"]

I tried few other recommendations and get this error

AccessDenied Invalid according to Policy: Policy Condition failed: ["eq", "Content-Type", "image/png"]

This is my cors configuration

<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>DELETE</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>

So how can I specify the content type in another way or fix this?

EDIT

When I omit any type of content-type (I don't specify any on the backend) I get a request payload like Ben's

------WebKitFormBoundaryUjxIo0gCBYg7AqJt
Content-Disposition: form-data; name="acl"

public-read
------WebKitFormBoundaryUjxIo0gCBYg7AqJt
Content-Disposition: form-data; name="awsaccesskeyid"

xxxx
------WebKitFormBoundaryUjxIo0gCBYg7AqJt
Content-Disposition: form-data; name="bucket"

xxxxx
------WebKitFormBoundaryUjxIo0gCBYg7AqJt
Content-Disposition: form-data; name="key"

images/Elf-Anime-Warrior-Wallpaper.jpg
------WebKitFormBoundaryUjxIo0gCBYg7AqJt
Content-Disposition: form-data; name="policy"

xxxx
------WebKitFormBoundaryUjxIo0gCBYg7AqJt
Content-Disposition: form-data; name="signature"

0BbpI0AP0wL3S1cBfo9n9tOp+N8=
------WebKitFormBoundaryUjxIo0gCBYg7AqJt
Content-Disposition: form-data; name="success_action_status"

201
------WebKitFormBoundaryUjxIo0gCBYg7AqJt
Content-Disposition: form-data; name="file"; filename="Elf-Anime-Warrior-Wallpaper.jpg"
Content-Type: image/jpeg


------WebKitFormBoundaryUjxIo0gCBYg7AqJt--

The issue is that in the bucket, the file has the content type as binary/octet-stream and so the image isn't viewed in the browser but instead it's downloaded (which is not what i want). So I'm not sure why Amazon is not setting the content type appropriately when my request payload looks like the above.

Upvotes: 9

Views: 11890

Answers (3)

13k
13k

Reputation: 299

According to Amazon S3 documentation, you can indeed pass the Content-Type as a form field, but you need to allow that in your policy.

So you'll need something like this:

Policy:

{
  "expiration": "2007-12-01T12:00:00.000Z",
  "conditions": [
    {"acl": "public-read" },
    {"bucket": "johnsmith" },
    ["starts-with", "$key", "user/eric/"],
    ["starts-with", "$Content-Type", ""],
    ...
  ]
}

The relevant line here is ["starts-with", "$Content-Type", ""]. The documentation states that a condition of type "match any" should be a "starts-with" with an empty value. If you want to restrict the content type to something, you should use ["starts-with", "$Content-Type", "image/"], for example.

Form:

(example taken from S3 documentation, change values accordingly).

<form action="http://examplebucket.s3.amazonaws.com/" method="post" enctype="multipart/form-data">
  <input type="hidden" name="key" value="user/user1/${filename}">
  <input type="hidden" name="acl" value="public-read">
  <input type="hidden" name="success_action_redirect" value="http://examplebucket.s3.amazonaws.com/successful_upload.html">
  <input type="hidden" name="Content-Type" value="binary/octet-stream">
  <input type="hidden" name="Policy" value='<Base64-encoded policy string>'>
  <input type="hidden" name="x-amz-meta-uuid" value="14365123651274">
  <input type="hidden" name="X-Amz-Signature" value="<signature-value>">
  <input type="hidden" name="X-Amz-Credential" value="AKIAIOSFODNN7EXAMPLE/20130806/us-east-1/s3/aws4_request">
  <input type="hidden" name="X-Amz-Algorithm" value="AWS4-HMAC-SHA256">
  <input type="hidden" name="X-Amz-Date" value="20130806T000000Z">

  Tags for File: 
  <input type="input" name="x-amz-meta-tag" value=""><br>
  File: 
  <input type="file" name="file"><br>

  <input type="submit" name="submit" value="Upload to Amazon S3">
</form>

As you already did in your first payload, you did set a "Content-Type" field for the form. This, together with the updated policy, should work.

As a side note though, if you know beforehand the type of the file you are uploading, you should set both the policy and the form field to that type. But if you are uploading files of different types, you set the Content-Type condition to "match any" in the policy and you can write a simple javascript intercepting the submit event of the form to dynamically change the Content-Type field (here written with the help of jQuery):

$("#s3-form").on("submit", function() {
  var contentTypeField = $("input[name='Content-Type']", this),
      fileField = $("input[type='file']", this),
      selectedFiles = fileField.get(0).files;

  if (files.length > 0) {
    // we're uploading a single file, so get the type for the first selected file
    contentTypeField.val(selectedFiles[0].type);
  }
});

Upvotes: 15

ben
ben

Reputation: 1462

Here's a sample working request payload when I upload a file to S3:

------WebKitFormBoundary9Vtjs2zgAF5jJgHC
Content-Disposition: form-data; name="key"

uploads/tmp/dbdfc12f-665e-4389-a739-f345e7acea48/svg_3927.zip
------WebKitFormBoundary9Vtjs2zgAF5jJgHC
Content-Disposition: form-data; name="AWSAccessKeyId"

xxx
------WebKitFormBoundary9Vtjs2zgAF5jJgHC
Content-Disposition: form-data; name="acl"

public-read
------WebKitFormBoundary9Vtjs2zgAF5jJgHC
Content-Disposition: form-data; name="policy"

xxx
------WebKitFormBoundary9Vtjs2zgAF5jJgHC
Content-Disposition: form-data; name="signature"

xxx=
------WebKitFormBoundary9Vtjs2zgAF5jJgHC
Content-Disposition: form-data; name="success_action_status"

201
------WebKitFormBoundary9Vtjs2zgAF5jJgHC
Content-Disposition: form-data; name="file"; filename="svg_3927.zip"
Content-Type: application/zip

As you can see the Content-type is set only once, together with the file data, at the end.

Update:

Here are the CORS for my dev bucket:

    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>Authorization</AllowedHeader>
    <AllowedHeader>Content-*</AllowedHeader>
    <AllowedHeader>Host</AllowedHeader>
    <AllowedHeader>*</AllowedHeader>

Notice <AllowedHeader>Content-*</AllowedHeader>, which might solve your problem.

Upvotes: 0

Scuzzy
Scuzzy

Reputation: 12332

Try offsetting ContentType within a 'prams' level array..

"acl" => "public-read",
"awsaccesskeyid" => $s3->getAwsAccessKeyId(),
"bucket" => $s3->getBucket(),
"key" => "images/" . $inputs['name'],
"policy" => $s3->getPolicy(true),
"signature" => $s3->getSignedPolicy(),
"success_action_status" => "201",
// indent "ContentType" within a "params" array
"params" => array(
    "ContentType" => $inputs['type']
)

Upvotes: -1

Related Questions