Fixing S3 Access Denied on Multipart Upload: Missing s3:AbortMultipartUpload Permission
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins
TL;DR
- What broke: Your IAM role or user has
s3:CreateMultipartUploadands3:PutObjectbut is missings3:AbortMultipartUpload. S3 rejects the abort call with403 Access Denied, leaving orphaned part data accumulating in the bucket. - How to fix it: Add
s3:AbortMultipartUploadto the IAM policy scoped toarn:aws:s3:::your-bucket/*. Add a bucket lifecycle rule to auto-abort incomplete uploads after N days as a hard backstop. - Shortcut: Use our Client-Side Sandbox above to auto-refactor your failing IAM policy — your ARNs never leave the browser.
The Incident (What Does the Error Mean?)
Raw error from AWS SDK or CLI:
An error occurred (AccessDenied) when calling the AbortMultipartUpload operation:
User: arn:aws:iam::123456789012:role/app-upload-role
is not authorized to perform: s3:AbortMultipartUpload
on resource: arn:aws:s3:::my-data-bucket/uploads/large-file.tar.gz
S3 multipart upload is a three-phase protocol: Initiate → Upload Parts → Complete or Abort. Most IAM policies are written to authorize the happy path (Initiate + PutObject + CompleteMultipartUpload) and forget the abort leg. When the SDK, a lifecycle rule, or your application logic tries to call AbortMultipartUpload — either on failure or during cleanup — S3 returns a hard 403. The upload is now a zombie: the parts exist, are billed, and cannot be removed by your application.
The Attack Vector / Blast Radius
This is a dual-threat failure: operational and financial.
Operational impact: Any retry logic that calls AbortMultipartUpload before re-initiating will permanently fail. Depending on your SDK's retry behavior, you may see cascading AccessDenied exceptions that surface as application-level upload failures even when the underlying network and data are fine. In high-throughput pipelines (ETL, media ingestion, ML dataset uploads), this silently corrupts your upload queue.
Financial blast radius: Orphaned multipart parts are billed at standard S3 storage rates. AWS does not automatically purge them. A single 5 GB file upload that fails and retries 10 times without abort leaves 50 GB of invisible, unindexed part data. At scale — a data pipeline doing 500 failed uploads/day — this compounds to terabytes within weeks. This is a well-documented AWS cost anomaly that has burned engineering teams on bills exceeding $10K/month before detection.
Privilege escalation angle: If your bucket policy or IAM policy uses a wildcard action (s3:*) as a "fix," you've introduced a far worse problem. An over-permissioned role can now delete objects, modify bucket policies, or exfiltrate data. Do not fix a missing permission with a wildcard.
How to Fix It (The Solution)
Basic Fix — Add the Missing Action
Locate the IAM policy attached to your upload role and add s3:AbortMultipartUpload to the existing S3 action block.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3MultipartUploadAccess",
"Effect": "Allow",
"Action": [
"s3:CreateMultipartUpload",
"s3:UploadPart",
"s3:CompleteMultipartUpload",
- "s3:PutObject"
+ "s3:PutObject",
+ "s3:AbortMultipartUpload",
+ "s3:ListMultipartUploadParts"
],
"Resource": "arn:aws:s3:::my-data-bucket/*"
}
]
}
Note:
s3:ListMultipartUploadPartsis included because most SDKs call it before aborting to enumerate parts. Missing it causes a secondary 403 on the same code path.
Enterprise Best Practice — Least Privilege with Condition Keys
Scope AbortMultipartUpload so a role can only abort uploads it initiated, not those belonging to other principals. Use the s3:ResourceAccount condition and, where your key structure allows, a prefix condition.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "S3MultipartUploadScoped",
"Effect": "Allow",
"Action": [
"s3:CreateMultipartUpload",
"s3:UploadPart",
"s3:CompleteMultipartUpload",
"s3:PutObject",
+ "s3:AbortMultipartUpload",
+ "s3:ListMultipartUploadParts"
],
- "Resource": "arn:aws:s3:::my-data-bucket/*"
+ "Resource": "arn:aws:s3:::my-data-bucket/uploads/${aws:PrincipalTag/ServiceName}/*",
+ "Condition": {
+ "StringEquals": {
+ "s3:ResourceAccount": "123456789012"
+ }
+ }
}
]
}
Additionally, add a bucket lifecycle rule as a hard backstop — this runs even if the IAM abort call fails:
# Terraform: aws_s3_bucket_lifecycle_configuration
resource "aws_s3_bucket_lifecycle_configuration" "multipart_cleanup" {
bucket = aws_s3_bucket.data_bucket.id
rule {
id = "abort-incomplete-multipart"
status = "Enabled"
+ abort_incomplete_multipart_upload {
+ days_after_initiation = 3
+ }
filter {
prefix = "uploads/"
}
}
}
The lifecycle rule is your last line of defense. It does not require IAM permissions from the application — S3 executes it internally.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Checkov static analysis — catches missing S3 multipart permissions in Terraform and CloudFormation before terraform apply:
checkov -d ./iam --check CKV_AWS_111,CKV_AWS_283
2. OPA/Conftest policy — enforce that any IAM policy granting s3:CreateMultipartUpload must also grant s3:AbortMultipartUpload:
package iam.s3.multipart
deny[msg] {
action := input.Statement[_].Action[_]
action == "s3:CreateMultipartUpload"
not action_present(input.Statement, "s3:AbortMultipartUpload")
msg := "IAM policy grants s3:CreateMultipartUpload without s3:AbortMultipartUpload. Orphaned parts risk."
}
action_present(statements, target) {
statements[_].Action[_] == target
}
3. AWS Config Rule — use iam-policy-no-statements-with-admin-access as a baseline and write a custom Config rule to flag upload roles missing the abort action.
4. Lifecycle rule as code — never provision an S3 bucket that accepts multipart uploads without the abort_incomplete_multipart_upload lifecycle rule. Enforce this in your Terraform module's variables.tf with a validation block:
variable "enable_multipart_lifecycle" {
type = bool
default = true
validation {
condition = var.enable_multipart_lifecycle == true
error_message = "Buckets accepting multipart uploads MUST have abort lifecycle rules enabled."
}
}