Initializing Enclave...

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:CreateMultipartUpload and s3:PutObject but is missing s3:AbortMultipartUpload. S3 rejects the abort call with 403 Access Denied, leaving orphaned part data accumulating in the bucket.
  • How to fix it: Add s3:AbortMultipartUpload to the IAM policy scoped to arn: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:ListMultipartUploadParts is 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."
  }
}

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →