The Localhost Trap: When Good CORS Intentions Go Bad

At GitHub Security Lab, misconfigured cross-origin resource sharing (CORS) remains one of the most frequently encountered vulnerability classes. CORS is the browser mechanism that lets a server relax the same-origin policy for specific external domains. Developers commonly widen these rules to integrate payment providers, social logins, or other third-party services—but the decisions made in those moments often introduce serious security holes.

Two related threats haunt this territory. First, a faulty or overly broad CORS policy can let an attacker’s website read responses from your server with the victim’s credentials. Second, DNS rebinding—a less familiar but similarly potent attack—can turn localhost into a weapon. Both allow an external site to impersonate a user against an application that trusts its own origin.

How CORS Works and Where It Breaks

CORS relies on two key response headers: Access-Control-Allow-Origin, which lists origins permitted to make cross-site requests and read responses, and Access-Control-Allow-Credentials, which governs whether the browser may attach cookies or HTTP authentication. Certain “simple” requests (GET, POST, HEAD with content types like application/x-www-form-urlencoded) are exempt from CORS preflight and can be sent cross-site even without these headers—but the response won’t be readable without the proper allow-origin header.

The danger lies in implementation. Developers often write a custom origin-checking middleware instead of using a battle-tested library, or they misconfigure a library’s defaults. The most common pitfalls involve string matching:

  • startsWith: If the allowlist contains https://stripe.com, an attacker registers https://stripe.com.attacker.com and passes the check.
  • endsWith: If the allowlist contains stripe.com, the domain attackerstripe.com becomes valid.
  • contains or HasPrefix: Even more permissive, often matching arbitrary substrings of the origin, which attackers can trivially spoof.

The secure pattern is a combination: exactMatch for apex domains like stripe.com, and an endsWith that checks for a leading dot (e.g., .stripe.com) to cover subdomains. One extra rule worth remembering: the null origin should never be on the allowlist. That value doesn’t just appear in privacy-sensitive contexts like file:// pages—it also comes from sandboxed iframes that an attacker can embed on a malicious page.

Exploitation and Impact

Successful CORS abuse lets an attacker perform actions on the victim’s behalf when the application relies on cookies with SameSite=None or on HTTP basic auth. While Chrome’s default of SameSite=Lax has somewhat mitigated these attacks, Firefox and Safari remain open to techniques that bypass tracking protection to deliver CORS-based attacks.

The resulting impact scales with the privilege of the compromised account. If an administrator is tricked into visiting a malicious page, the attacker could gain the ability to execute scripts or binaries on the server host—yielding remote code execution (RCE). A faulty CORS policy is also a force multiplier for other bugs: with admin-level access, an attacker can reach sensitive endpoints, abuse arbitrary file writes, or pivot to other services on the same network.

Case Study: CORS Flaw Leads to RCE

Cognita, a Python tool for testing retrieval-augmented generation with LLMs, ships a FastAPI backend. Its CORS middleware had unsafe defaults: allow_origins set to all origins and allow_credentials set to true. Normally a browser ignores the wildcard when credentials are involved, but this specific middleware reflected the requesting origin back even with credentials sent.

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

The practical consequence is severe: an attacker’s website can send authenticated requests to any Cognita endpoint the victim can reach—including instances running on localhost or on an internal network. No authentication existed at the time, but the vulnerability meant that even future authentication could be bypassed entirely.

The attack progressed further when we found an arbitrary file write endpoint meant for Docker users. The file.filename parameter was never sanitized, and os.path.join resolved the .. sequences, granting full control over the destination path.

@router.post("/upload-to-local-directory")
async def upload_to_docker_directory(
    upload_name: str = Form(
        default_factory=lambda: str(uuid.uuid4()), regex=r"^[a-z][a-z0-9-]*$"
    ),
    files: List[UploadFile] = File(...),
):
...
        for file in files:
            logger.info(f"Copying file: {file.filename}, to folder: {folder_path}")
            file_path = os.path.join(folder_path, file.filename)
            with open(file_path, "wb") as f:
                f.write(file.file.read())

With a file write primitive in hand, we looked at the official Dockerfile for a target.

command: -c "set -e; prisma db push --schema ./backend/database/schema.prisma && uvicorn --host 0.0.0.0 --port 8000 backend.server.app:app --reload"

The backend was started with --reload, meaning uvicorn automatically restarts whenever any Python file changes. Overwriting an __init__.py or similar startup file would trigger a restart and execute our payload, yielding RCE on the server. Beyond Cognita itself, that foothold becomes a launching pad for lateral movement inside the network.

Case Study: Faulty Domains and Backdoor Accounts

The tamagui.dev site exposed another pattern: hardcoded endpoints in its CORS middleware that suggested a developer adding origins as new needs arose—likely Stripe for payments, localhost for development, and the main domain for subdomains or SSL issues.

export function setupCors(req: NextApiRequest, res: NextApiResponse) {
  const origin = req.headers.origin

  if (
    typeof origin === 'string' &&
    (origin.endsWith('tamagui.dev') ||
      origin.endsWith('localhost:1421') ||
      origin.endsWith('stripe.com'))
  ) {
    res.setHeader('Access-Control-Allow-Origin', origin)
    res.setHeader('Access-Control-Allow-Credentials', 'true')
  }
}

Checking those hardcoded origins with endsWith logic created the same blind spot we noted earlier. An attacker could register a domain that happened to end with one of the allowed strings and then act as the user—potentially charging a credit card or modifying account data if permissions allowed.

Finally, a Go project in the wild demonstrates how low security can rank on a project’s priority list. Its origin check relied on the HasPrefix and Contains functions, effectively validating almost anything sent in the Origin header.

func CorsFilter(ctx *context.Context) {
    origin := ctx.Input.Header(headerOrigin)
    originConf := conf.GetConfigString("origin")
    originHostname := getHostname(origin)
    host := removePort(ctx.Request.Host)

    if strings.HasPrefix(origin, "http://localhost") || strings.HasPrefix(origin, "https://localhost") || strings.HasPrefix(origin, "http://127.0.0.1") || strings.HasPrefix(origin, "http://casdoor-app") || strings.Contains(origin, ".chromiumapp.org") {
        setCorsHeaders(ctx, origin)
        return
    }

func setCorsHeaders(ctx *context.Context, origin string) {
    ctx.Output.Header(headerAllowOrigin, origin)
    ctx.Output.Header(headerAllowMethods, "POST, GET, OPTIONS, DELETE")
    ctx.Output.Header(headerAllowHeaders, "Content-Type, Authorization")
    ctx.Output.Header(headerAllowCredentials, "true")

    if ctx.Input.Method() == "OPTIONS" {
        ctx.ResponseWriter.WriteHeader(http.StatusOK)
    }
}

That looseness allowed an attacker to craft a link like https://localhost.attacker.com. If an administrator clicked it, the malicious site could call the user-add endpoint and create a backdoor account with full privileges—no further exploits required.

Lessons From the Trenches

Three takeaways emerge from these cases. First, avoid hand-rolling CORS logic unless you understand the edge cases of prefix, suffix, and substring matching—the secure combination of exactMatch and dot-prefixed endsWith is not obvious. Second, never assume a CORS framework is safe by default; check whether it reflects a wildcard or an actual origin when credentials are allowed. Third, treat localhost and locally hosted services as untrusted surfaces—applications meant for intranets are exactly the sort that attackers will hunt for with DNS rebinding or internal IP enumeration. In all cases, a good origin check is just the first layer of defense; peer with secure cookie settings and robust server-side authorization to keep the user’s browser from becoming an attack proxy.

When DNS becomes the attack vector

DNS rebinding exploits the same underlying weakness as CORS misconfigurations—allowing a browser to send requests to unintended origins—but it requires no developer mistake. Instead, it abuses the DNS resolution process itself.

An attacker first lures a victim’s browser to a domain serving malicious JavaScript. That script then makes requests to a host the attacker controls, but the attacker repeatedly changes the domain’s DNS records to point at different local IP addresses. By cycling through addresses, the malicious script scans for open connections and sends payloads to whichever local services respond. The singularity tool from NCCGroup automates much of this setup; its payloads folder contains scripts that dictate how requests are sent and how responses are handled, and custom scripts can be added.

Mitigation is straightforward because DNS rebinding cannot carry cookies. Any sensitive or privileged endpoint protected by authentication will reject the request, since the browser sends cookies belonging to the attacker’s domain rather than the real application’s. For simple applications without authentication, the alternative is to verify the Host header against an approved hostname or local name. Many new AI projects lack either protection, leaving their data retrievable and their vulnerabilities remotely exploitable.

   public boolean isValidHost(String host) {

        // Allow loopback IPv4 and IPv6 addresses, as well as localhost
        if (LOOPBACK_PATTERN.matcher(host).find()) {
            return true;
        }

        // Strip port from hostname - for IPv6 addresses, if
        // they end with a bracket, then there is no port
        int index = host.lastIndexOf(':');
        if (index > 0 && !host.endsWith("]")) {
            host = host.substring(0, index);
        }

        // Strip brackets from IPv6 addresses
        if (host.startsWith("[") && host.endsWith("]")) {
            host = host.substring(1, host.length() - 2);
        }

        // Allow only if stripped hostname matches expected hostname
        return expectedHost.equalsIgnoreCase(host);
    }

Security scanners rarely flag DNS rebinding because the conditions required for a successful attack lead to too many false positives. At GitHub, reports to maintainers frequently go unfixed for the same reason; only the most widely used repositories tend to have checks in place. For software that stores security-critical data or performs privileged operations, we recommend code that explicitly verifies the Origin header matches the Host or an allowlist.

Final checks for local service safety

CORS has always been a fertile source of same-origin-policy bypasses, but the mechanics are well understood, making detection and remediation relatively uncomplicated. Newer browser-level protections reduce some exposure and may eventually remove the bug class entirely. A quick search for CORS or Access-Control-Allow-Origin in your codebase often surfaces insecure presets or flawed logic.

The Mozilla Developer Network CORS page is a solid reference for how CORS works and for configuring a CORS framework properly. For applications that lack authentication but handle sensitive operations, verifying the Host header adds an important layer of defense. GitHub Code Security can automate the search and suggest fixes for issues like CORS misconfiguration for credentials.