Tutorials Web Security Series Chapter 1

Subdomain Takeover — How a Deleted S3 Bucket Let a Stranger Serve a Scam on My Domain

SecurityChapter 1 of the Web Security Series26 minJune 10, 2026Intermediate

One morning a domain I owned was serving a gambling site I had never built. Same domain, same green padlock in some browsers, my brand name in the address bar — and a wall of flashing slot-machine graphics in a language I don't speak. I didn't get phished. Nobody guessed a password. No server of mine was breached, because by then I didn't even have a server at that address. The attacker never touched my Cloudflare account, my registrar, or my AWS console.

They didn't need to. They just claimed something I had thrown away and forgotten to unplug.

This is the most common web attack almost nobody teaches in a beginner course: subdomain takeover via a dangling DNS record. It's not glamorous, it doesn't involve a single line of injected code, and it happens to real sites — including, in this case, one of mine — usually right after a migration. This chapter is the full, redacted post-mortem: what was wired, what I deleted, the mistake an AI coding agent and I made together during the move, exactly how a stranger turned a leftover DNS line into control of my homepage, how I put it out, and the short checklist that would have made the whole thing impossible.

The 30-second version

If you only remember one paragraph, make it this one.

A DNS record on yoursite.com pointed at an Amazon S3 bucket. I later deleted that bucket — but I left the DNS record behind, still pointing at where the bucket used to live. That leftover record is called a dangling record. Because S3 bucket names live in a single, global, first-come-first-served namespace, a stranger was able to create a brand-new bucket with the exact same name, and from that moment every visitor to my domain was served their files instead of mine. The fix was one DNS edit. The lesson is an ordering rule and a checklist.

Loading diagram…

Figure 1 — Six steps from a healthy site to a hijacked one. Note that nothing on my side was "hacked." The only mistake was step C: a record that outlived the thing it pointed at.

How the site was wired (the redacted real config)

To see the hole you have to see the plumbing. The site was a plain static site — HTML, CSS, a little JavaScript — hosted directly out of an Amazon S3 bucket using S3 static website hosting. The domain was managed at Cloudflare, in DNS-only mode (the grey cloud, not the orange one — we'll come back to what that means). The two records that matter looked like this:

Type    Name           Content                                          Proxy      TTL
CNAME   yoursite.com   yoursite.com.s3-website.<region>.amazonaws.com   DNS only   Auto
CNAME   www            your-account.coursehost.com                      DNS only   1 min

Two records, two third parties. The apex (yoursite.com) pointed at S3. The www subdomain pointed at a hosted course platform. Both are completely ordinary, and both are takeover candidates the instant their backing resource disappears. Hold that thought.

The apex record is the interesting one, so let's read it slowly. (If "CNAME", "apex", and "DNS" are fuzzy, Ch 1 of the Web series walks through the whole name-resolution loop from zero.)

So the chain, while everything was healthy, was: visitor types yoursite.com → Cloudflare answers with the S3 endpoint → S3 sees the Host: yoursite.com header → S3 finds the bucket named yoursite.com → serves my index.html. Clean. Boring. Exactly how it's supposed to work.

The fragility is hiding in plain sight: the only thing tying my domain to my content is a bucket name — a string in a shared, global namespace. Keep pulling that thread.

The migration that opened the door

Months later I moved the site off AWS and onto Cloudflare's stack (the same Workers + Pages world this very site runs on — see the dissection of simpleappshipper.com). Cheaper, simpler, no more S3 bill. A normal, sensible migration. The teardown went like this:

  1. Rebuilt the site on Cloudflare.
  2. Deleted the S3 bucket named yoursite.com. (Why keep paying for a bucket I'd never use again? It felt like good housekeeping. It was the opposite.)
  3. Closed out the old AWS resources.
  4. Moved on.

Spot the missing step. I deleted the resource but I never deleted the DNS record that pointed at it. The apex CNAME still said, to the entire internet, "for yoursite.com, go to the S3 endpoint and ask for the bucket named yoursite.com." Except now there was no such bucket. The record was dangling — a signpost to an empty lot.

The AI-agent twist

Here's the part that makes this incident more than a generic war story, and the reason it belongs on this site. I didn't do the migration entirely by hand — I did it alongside an AI coding agent, the same way most of this project gets built. And the agent had memory of the old setup: notes about the S3 bucket, the region, the endpoint, the exact bucket name.

During the cutover, that memory worked against me. When we reasoned about DNS, the agent helpfully recalled the old S3 endpoint from its notes and treated it as still-current infrastructure — reaffirming the dangling record instead of flagging it for deletion. It wasn't malicious or even wrong-sounding; it was confidently operating on a fact that used to be true. A deleted bucket is exactly the kind of thing that lingers in an agent's context long after it's gone from the world. The human (me) didn't catch it either, because the record looked right — it had always been there.

Loading diagram…

Figure 2 — The quiet failure mode: a true-yesterday fact survives in memory and gets re-applied today. Nobody "made a mistake" in the dramatic sense. The bucket was simply remembered into a plan it should have been deleted out of.

Why a deleted bucket is a loaded gun — the global namespace

A dangling record is only dangerous if a stranger can step into the empty lot. With S3, they absolutely can, and the reason is a design decision baked into the service:

Amazon S3 bucket names are globally unique across every AWS account on Earth — and when you delete a bucket, the name is released back into the pool for anyone to claim.

There is no "this name belonged to you, so it's reserved." The moment my yoursite.com bucket was deleted, that name became available again to the first AWS customer who typed it into the create-bucket box. That customer was not me.

How did a stranger know to try? They didn't guess. Dangling records are discoverable at scale. Attackers (and security researchers, and bots) continuously sweep the internet for them using:

So the attacker's whole job was: scan, spot NoSuchBucket on a live domain, open the AWS console, create a bucket named yoursite.com in the right region, enable static website hosting, upload an index.html full of casino graphics, and wait. The dangling CNAME did the rest — it cheerfully kept routing my visitors straight to the new owner of the name.

Loading diagram…

Figure 3 — After takeover, every step is still "working correctly." DNS resolves, S3 routes by Host, the bucket exists. The chain is intact — it's just been re-pointed at someone else's files by re-using a name I gave up.

What the attacker actually gets

"They put up a gambling page" undersells it. When someone controls the content served at your domain, they inherit every bit of trust that domain has accumulated:

The severity is "high" for a reason: zero code was injected, yet the blast radius is your entire domain's trust.

Mini-exercise: read the takeover fingerprint yourself

You don't need to be under attack to see the signature. Point curl at any S3 website host for a bucket that doesn't exist and read the body — this is exactly what a scanner keys on:

# A request to an S3 website endpoint for a non-existent bucket
curl -s -o - -w "\nHTTP %{http_code}\n" \
  "http://does-not-exist-$RANDOM-bucket.s3-website.us-east-1.amazonaws.com/"

You'll get an HTTP 404 and a body containing NoSuchBucket and "The specified bucket does not exist." Now imagine that response coming back for a request to a real, live domain. That mismatch — live domain, "there's nothing here" backend — is the entire detection rule. Write it down; you'll automate it later in the checklist.

Does the grey cloud matter?

Both records were DNS only (grey cloud), meaning Cloudflare answered the DNS query but did not proxy the traffic — visitors connected straight through to the S3 endpoint. People reasonably ask: "would the orange cloud (proxied) have saved you?"

Honest answer: not on its own. If I'd proxied the apex record, Cloudflare would have sat in front and given me a WAF, caching, and a hidden origin — all good things — but it would have been proxying to the same dangling endpoint. The takeover happens at the name-resolution / origin layer, upstream of the proxy. Re-pointing a proxied record at a resurrected attacker bucket still serves the attacker.

So treat proxying as defense-in-depth, not the fix:

The real fix lives one layer up, in the records themselves. Let's go put the fire out, then make sure it can't start.

Putting out the fire — incident response

When you discover a live takeover, work in this order. The first step stops the bleeding in minutes; the TTL on that record was Auto/1 min, which (for once) works in your favour — changes propagate fast.

  1. Cut or re-point the dangling record — now. In Cloudflare DNS, delete the offending CNAME, or point it at an origin you actually control (a Pages project, a Worker, a holding page). The instant resolvers pick up the change, visitors stop reaching the attacker. This is the one that matters; do it first, investigate after.
  2. Deny them the name back. If you'll keep using S3 at all, immediately create a bucket with that exact name in that region yourself so the attacker can't simply re-grab it during your cleanup. Better: stop pointing at a claimable third-party name entirely.
  3. Purge caches. Purge Cloudflare's cache (and any CDN) so no edge node keeps serving the scam after the origin is fixed.
  4. Rotate anything that touched the bad page. If the attacker's page lived on your domain alongside cookies or tokens, assume exposure and rotate. Check Certificate Transparency logs (e.g. crt.sh) for certificates issued for your hostname that you didn't request.
  5. Clean up the search-engine damage. In Google Search Console: use the Removals tool on the scam URLs, check the Security Issues report, and — if the domain got flagged — request a Safe Browsing review once it's clean. Re-submit your sitemap.
  6. Sweep the rest of the zone. A takeover is a symptom; the disease is "records that outlive their backends." Audit every record (don't forget that www → coursehost.com line) for the same pattern. Which brings us to the part you should do before any of this is ever necessary.

The pre-flight checklist — what to check BEFORE you get attacked

This is the section I wish someone had handed me. None of it is advanced. All of it would have made the incident impossible.

Mini-exercise: audit your own zone in 10 minutes

Open your DNS provider and answer three questions, in writing:

  1. Which records point at a third party? List every CNAME/ALIAS whose content is a hostname you don't own outright.
  2. For each, does the backend still exist and do I still own it? Open the S3 bucket / GitHub repo / SaaS custom-domain setting and confirm. Any "hmm, not sure" is a finding.
  3. What's my teardown order? Write the one sentence: "When I retire a service, I delete its DNS record before I delete the service." Pin it where your next migration will happen.

If that audit turns up even one record you can't fully account for, you just found your yoursite.com before a scanner did.

Challenges

  1. Reproduce the fingerprint, safely. Using only your own resources, stand up an S3 (or GitHub Pages) site, point a test subdomain at it, then delete the backend and watch the takeover signature appear with curl. Re-point the record to fix it. You'll never forget the NoSuchBucket body once you've made it appear on purpose.
  2. Write a one-file monitor. Sketch a tiny scheduled script (a Cloudflare Cron Trigger + Worker is perfect) that fetches each at-risk host from a list and alerts you if the response body contains any known takeover fingerprint. What's the smallest version that would have caught my incident on day one?
  3. Design the safe teardown. Write the exact, ordered runbook you'd hand an AI agent for "retire the old S3 site and move to Cloudflare" such that a dangling record is structurally impossible. Where in your steps does a human have to confirm a fact the agent can't?
  4. Find the second takeover. Given the www → your-account.coursehost.com record, describe precisely how a takeover of that subdomain would differ from the S3 one, and what platform-side feature (domain verification) would block it.

Key Points

Takeover is the gentlest member of a family of attacks that all share one root cause: trusting something you don't actually control. This chapter was the anatomy — how one takeover works once it's happened. Chapter 2 is the field work: the exact outside-in method to tell a real compromise from a mere misconfiguration the moment a domain of yours starts misbehaving, the full map of every dangling-pointer variant beyond S3, and a 15-minute audit that finds them on every domain you own before a scanner does. The thread that runs through all of it is the same one that started here: know exactly what your site depends on, and never leave a signpost pointing at an empty lot.

← Series OverviewCh 2: Is my domain hacked? — investigate, then audit
InfrastructureInternet Infrastructure SeriesPractical internet infrastructure concepts: DNS, RDAP, WHOIS, IP allocation, ASNs, and registries.Production WebProduction Web Apps SeriesProduction patterns for web apps: caching, rate limiting, webhooks, queues, cron jobs, and idempotency.CloudflareCloudflare Feature FocusFocused Cloudflare tutorials for Workers, R2, Stream, Durable Objects, and edge deployment.

Ship your apps faster

When you're ready to publish your Swift app to the App Store, Simple App Shipper handles metadata, screenshots, TestFlight, and submissions — all in one place.

Try Simple App Shipper
5 free articles remainingSubscribe for unlimited access