Category: Deep Dives

Long form technical deep dives into one mechanism at a time: cloud, kernel, IoT, and privacy internals.

  • What is Web Cache Poisoning? How One Request Hits Many Users

    What is Web Cache Poisoning? How One Request Hits Many Users

    A cache sits in front of a web app to make pages fast: it stores a response once and hands the same copy to everyone who asks for the same thing. Web cache poisoning abuses that sharing. An attacker sends one carefully shaped request that makes the origin return a harmful response, gets the cache to store it under a normal key, and then every later visitor who hits that key is served the attacker’s version. One request, many victims.

    Caches, cache keys, and unkeyed inputs

    A cache decides whether two requests are the same by building a cache key. Most caches build that key from a small set of fields: the method, the host, the path, and sometimes the query string. If a later request produces the same key, the cache replies from storage instead of asking the origin again.

    # Two requests the cache treats as identical (same key)
    GET /promo HTTP/1.1
    Host: notes.acme.example
    
    # Cache key (simplified): GET + notes.acme.example + /promo
    

    The trap is everything the cache leaves out of the key. Headers like X-Forwarded-Host, X-Forwarded-Scheme, cookies, or a custom header are usually not part of the key. These are unkeyed inputs. If an unkeyed input changes the response but does not change the key, the cache will happily store a response that depends on a value it ignored. That gap is the whole attack.

    If an input changes the response but not the cache key, the cache will store one person’s response and serve it to the next person.

    How this differs from web cache deception

    These two bugs sound alike and are not. In web cache deception, the attacker tricks the cache into storing a victim’s private response (a profile page, an account API reply) so the attacker can read it. The harm flows toward the attacker. Web cache poisoning is the reverse: the attacker plants a harmful response in the cache so it is served to other users. The harm flows outward, from one attacker to a crowd.

    How a web cache poisoning attack works

    Take Acme Notes, a typical SaaS app at notes.acme.example behind a CDN. The origin builds some absolute URLs using the incoming X-Forwarded-Host header, so it can run behind different front ends. The CDN does not include that header in its cache key. That is the unkeyed input.

    The attacker probes by sending a value they can recognize later:

    GET /promo HTTP/1.1
    Host: notes.acme.example
    X-Forwarded-Host: evil.example
    
    HTTP/1.1 200 OK
    X-Cache: miss
    Cache-Control: public, max-age=300
    ...
    <link rel="canonical" href="https://evil.example/promo">
    <script src="https://evil.example/static/app.js"></script>
    

    The origin reflected evil.example into the page and told the cache to keep the response for 300 seconds. Because the header was unkeyed, the cache stored this poisoned copy under the plain key for /promo. Now a normal visitor asks for the page with no special headers at all:

    GET /promo HTTP/1.1
    Host: notes.acme.example
    
    HTTP/1.1 200 OK
    X-Cache: hit
    Age: 42
    ...
    <script src="https://evil.example/static/app.js"></script>
    

    The victim never sent the malicious header. They get the poisoned response because the cache is serving the stored copy. The X-Cache: hit and the rising Age value confirm the response came from cache, not the origin.

    What an attacker can do with it

    • Stored XSS through a reflected unkeyed header. If the origin reflects an unkeyed header into HTML without encoding it, the attacker poisons the page with a script tag or event handler. Unlike normal reflected XSS, the victim does not need to click a crafted link. They just load the page, and the cache feeds them the script.
    • Redirect to an attacker site. When the origin uses an unkeyed header to build a redirect or a canonical URL, the poisoned response can point users to evil.example. This overlaps with host header injection, since both abuse the app trusting a host value it should not.
    • Denial of service through a poisoned error. An oversized header or an unkeyed value that triggers a 400 or 500 can get the error response cached under a normal key. Every visitor then receives the cached error until it expires, taking the page down without touching the origin.

    How to detect web cache poisoning

    Detection has two halves: find the unkeyed inputs, then watch the cache react.

    • Hunt for unkeyed inputs. Against an app you own, add one candidate header at a time (X-Forwarded-Host, X-Forwarded-Scheme, X-Forwarded-For, X-Host, and any custom header the app reads) with a unique marker value. If the marker shows up in the response body, headers, or a redirect, that header influences the output.
    • Confirm it is unkeyed. Send the same request twice, once with the marker and once without, and compare cache behavior. Watch X-Cache (hit or miss), Age, and any Vary header. If a clean request later returns your marker with X-Cache: hit, the response was cached under a key that ignored your header. That is a confirmed poison path.
    • Read the cache control signals. A Vary header tells you which request headers the cache does include in the key. If a header that changes the response is missing from Vary, it is a candidate. Use a cache buster like /promo?cb=12345 in tests so you never poison a real shared key while probing.

    How to prevent web cache poisoning

    • Do not reflect unkeyed input into cached responses. If a header is not in the cache key, treat its value as untrusted and keep it out of anything the cache will store: HTML, redirects, canonical tags, and link or script sources.
    • Key on or strip security relevant headers. If the app genuinely needs X-Forwarded-Host or similar, add it to the cache key with Vary or your CDN’s key settings so different values cache separately. If the app does not need it, strip the header at the edge before it ever reaches the origin.
    • Cache only truly static content. Pin caching to assets that do not depend on request specific input, like images, CSS, and versioned scripts. Mark dynamic pages Cache-Control: no-store or private so they are never shared.
    • Scope caching carefully. Avoid a broad rule that caches every 200 response. Decide per route what is cacheable, and never let error responses for one user persist under a shared key.

    Why web cache poisoning rewards understanding the app

    You do not find this bug by firing a fixed payload list at a target. You find it by understanding which headers the origin reads, which of them the cache ignores, and whether a value one user sends can land in a response another user receives. The flaw is an assumption: that every input affecting the response is also part of the cache key. Test that assumption directly and the gap shows itself.

    That is the kind of bug an autonomous researcher that tests an app’s assumptions is built to surface, since it lives in the seam between two systems rather than in a single known payload. You can read more about that approach on our about page.

    Frequently asked questions

    What is web cache poisoning?

    It is an attack where someone sends a crafted request that makes the origin server return a harmful response, then gets a shared cache to store that response under a normal cache key. Every later visitor who hits the same key is served the poisoned copy. The trick relies on an unkeyed input, usually a header like X-Forwarded-Host, that changes the response but is left out of the cache key.

    How is web cache poisoning different from web cache deception?

    They move harm in opposite directions. In web cache deception, the attacker tricks the cache into storing a victim’s private response so the attacker can read it, so harm flows toward the attacker. In web cache poisoning, the attacker plants a harmful response in the cache so it is served to many other users, so harm flows outward from one attacker to a crowd.

    What is an unkeyed input?

    A cache key is built from a small set of request fields, usually the method, host, path, and sometimes the query string. Any input the cache leaves out of the key is unkeyed: common examples are X-Forwarded-Host, X-Forwarded-Scheme, cookies, and custom headers. If an unkeyed input changes the response, the cache can store a response shaped by a value it ignored, which is the gap web cache poisoning exploits.

    How do you detect and prevent web cache poisoning?

    To detect it, add one candidate header at a time with a unique marker against an app you own, see if the marker is reflected, then check whether a clean request later returns it with X-Cache: hit and a rising Age. To prevent it, do not reflect unkeyed input into cached responses, add security relevant headers to the cache key or strip them at the edge, cache only truly static content, and scope caching per route instead of caching every 200 response.

  • Kubernetes service account token abuse: from one pod to cluster admin

    Kubernetes service account token abuse: from one pod to cluster admin

    Every pod in a default Kubernetes cluster gets handed a small file it never asked for. That file is a Kubernetes service account token, and it sits at a fixed path inside the container, ready for any process that can read the filesystem. The token lets the pod talk to the API server, which is fine when the pod needs that. The trouble starts when an attacker who lands code execution in one pod, or who can make that pod issue requests for them, picks the token up and starts walking toward cluster admin. This post takes that walk apart, from the mounted file to the RBAC rights that turn one compromised pod into a foothold across the whole cluster.

    Why a pod has a Kubernetes service account token at all

    When you create a pod and say nothing about identity, Kubernetes assigns it the default service account in its namespace and mounts that account’s token into the container. Look inside a running pod for our invented cluster at acme.example and you find this:

    /var/run/secrets/kubernetes.io/serviceaccount/token
    /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
    /var/run/secrets/kubernetes.io/serviceaccount/namespace

    The token file holds a signed JSON Web Token, and ca.crt lets the pod trust the API server. The token is a bearer credential, so whoever holds it is treated as the account that owns it, with no second factor. This is reasonable when a pod has a real reason to call the API, for example a controller that watches config maps. The problem is that many pods get a token they never use, because auto mount is on by default, and a credential nobody needs is still one to steal.

    From one compromised pod to the API server

    An attacker reaches the token in one of two ways. The loud way is code execution: an application bug or a vulnerable dependency gives them a shell, and reading a file is then trivial. The quieter way is server side request forgery, where the app is tricked into making an HTTP request to a destination the attacker chooses. We cover that in our writeup on SSRF, and a third route in container escape. Each ends the same way: the token leaves the pod.

    Inside the cluster the API server is reachable at a stable endpoint, exposed through the kubernetes service and environment variables every pod receives, for example KUBERNETES_SERVICE_HOST=10.0.0.1 on port 443. With the token and that address, the request is simple. The token rides in the Authorization header:

    GET https://10.0.0.1:443/api/v1/namespaces/acme-prod/secrets
    Authorization: Bearer <contents of the token file>

    If the service account may list secrets in that namespace, the API server answers honestly. It does not care that the request came from a process the attacker now controls. The token is valid, so the call is authorized.

    A mounted token is not a secret the way a password is a secret. It is a working key to the API server, sitting in plain sight inside every pod that was told to carry one.

    How excessive RBAC turns a token into escalation

    A stolen token is only as useful as the rights attached to it. Role based access control, or RBAC, decides what each service account may do, and escalation lives in how generous those rules are. The first move an attacker makes is to ask the API server what the token can do:

    kubectl auth can-i --list

    That returns the verbs and resources the account holds. A few common over grants and what each buys an attacker:

    • list or get on secrets reads every secret in scope, often including database passwords, API keys, and other service account tokens. One read can hand over credentials that reach far past the cluster.
    • create on pods lets the attacker launch a pod they design. One that mounts the host filesystem or runs as privileged is a direct route off the node.
    • create on rolebindings or clusterrolebindings lets them bind a stronger role to an account they control. Bind cluster-admin and the walk is over.
    • create on pods/exec lets them run commands inside other running pods, including ones in other namespaces, spreading sideways.

    The worst case is an application service account carrying a wildcard verb on a wildcard resource, or a binding straight to cluster-admin. Then the difference between a contained incident and a full takeover is one stolen token. The token did not gain new rights. It was always a key to whatever RBAC allowed.

    The metadata and SSRF angle on managed clusters

    On managed clusters there is a second prize. A pod an attacker can steer can often reach the cloud metadata endpoint at the link local address 169.254.169.254, the same endpoint we take apart in our post on the instance metadata service. If the node’s identity is over permissioned, the credentials parked there extend the blast radius into the cloud account. An attacker probing SSRF tries the in cluster API address and the metadata IP in many encoded forms, hoping one slips past a filter. A free in browser tool, the SSRF IP and URL normalizer, shows how those internal addresses can be rewritten, which helps a defender see what a blocklist must catch.

    Detecting and preventing the abuse

    The fixes stack, and none of them depend on catching every application bug first. Each control shrinks either the chance a token leaks or the damage it does once it has.

    Stop mounting tokens that nobody uses

    If a pod never calls the API server, it has no reason to carry a token. Turn auto mount off, on the service account or pod spec, so the file is never there to steal:

    automountServiceAccountToken: false

    This is the highest value single change for the many workloads that never talk to Kubernetes. A token never mounted cannot be read or leaked at all.

    Practice least privilege in RBAC

    Give each service account only the verbs and resources its job requires, scoped to one namespace where possible. No wildcard verbs, no wildcard resources, and no binding an application account to cluster-admin. Audit the bindings you have, because clusters accumulate broad grants as people copy an example that asked for too much. Read access to secrets deserves a hard look, since one list call drains a namespace.

    Use bound, short lived tokens and segment the cluster

    Modern Kubernetes issues projected tokens bound to a specific pod that expire on a short clock, so a stolen copy stops working on its own. Prefer those over old style tokens that never expired. Put sensitive workloads in their own namespaces so a foothold in one does not see another’s secrets. Apply a network policy that blocks pod access to the metadata endpoint and restricts egress, so even a steered pod cannot reach 169.254.169.254. The CNCF and the joint NSA and CISA Kubernetes hardening guidance treat these controls as a baseline.

    The assumption that breaks

    Strip away the JSON and the headers and what is left is one assumption. Kubernetes mounts a Kubernetes service account token because it assumes the only thing reading that file is the pod’s own honest code. An application bug breaks that: the moment an attacker can run code or forge a request inside the pod, they can read anything the pod can read and call anything it can call. The boundary everyone pictured, the wall around the container, was not the one that mattered. The one that mattered ran through an RBAC rule that granted too much. You find that kind of gap by asking what each component trusts and why, not by scanning for a known bad string. There are more teardowns like this on the blog.

    This is the class of bug an autonomous researcher that tests an application’s assumptions is built to find. UnboundCompute is early and still being built, so we will say only that it does the honest work of mapping trust. Read more on our about page.

    Frequently asked questions

    Where does Kubernetes mount the service account token inside a pod?

    By default the token is projected into the container at /var/run/secrets/kubernetes.io/serviceaccount/token, alongside ca.crt and a namespace file. It is a signed bearer token, so any process that can read that path can present it to the API server and be treated as the service account that owns it.

    How does a stolen service account token lead to escalation?

    The token only carries the rights granted to its account through RBAC. If that account has over broad rules such as list on secrets, create on pods, or create on rolebindings, an attacker can read credentials, launch a privileged pod, or bind a stronger role. A binding to cluster-admin turns one stolen token into full cluster control. See the Kubernetes RBAC docs at https://kubernetes.io/docs/reference/access-authn-authz/rbac/.

    How do I stop pods from carrying a token they do not need?

    Set automountServiceAccountToken to false on the service account or the pod spec for any workload that never calls the API server. A token that was never mounted cannot be read by a shell or leaked through SSRF, which removes the credential from the many pods that have no reason to talk to Kubernetes at all.

    Can SSRF in a pod reach the cloud metadata endpoint?

    Yes. A pod an attacker can steer through SSRF can often reach both the in cluster API server and the cloud metadata endpoint at 169.254.169.254. If the node identity is over permissioned, the credentials there extend the reach from the cluster into the cloud account. Block the metadata IP with a network policy and restrict egress.

  • SAML Signature Wrapping Explained: When a Valid Signature Lies

    SAML Signature Wrapping Explained: When a Valid Signature Lies

    SAML signature wrapping is an attack on single sign on that turns a valid signature into a lie about who you are. The identity provider signs an XML assertion that says “this user is alice.” The attacker captures that signed assertion and rearranges the document so the signature still checks out over the original element, while the service provider reads a second, injected assertion that says “this user is admin.” The signature is valid. The thing the application uses is not the thing that was signed. This post explains SAML signature wrapping from the ground up, shows the shape of a wrapped document, and lists the defenses that actually close it.

    How SAML single sign on works

    SAML is the protocol that lets you log in to one place and reach many apps without typing a password at each one. Three parties take part. The user in a browser, the service provider (the app you want to use, call it acme.example), and the identity provider (the trusted login system that vouches for who you are).

    The flow is short. You hit acme.example. It does not know you, so it bounces your browser to the identity provider. You authenticate there. The identity provider builds an XML document called an assertion that states your identity and signs it with an XML digital signature. Your browser carries that signed assertion back to acme.example. The service provider checks the signature, sees it was issued by a provider it trusts, and logs you in as whoever the assertion names.

    A trimmed assertion looks like this. The Assertion element carries an ID, and the Signature block points at that ID with a Reference, saying “I cover the element whose id is _abc123.”

    <Response>
      <Assertion ID="_abc123">
        <Subject><NameID>alice@acme.example</NameID></Subject>
        <Signature>
          <Reference URI="#_abc123"/>
          <SignatureValue>...</SignatureValue>
        </Signature>
      </Assertion>
    </Response>

    Why a valid signature is not enough

    Here is the gap that SAML signature wrapping lives in. Two separate pieces of code look at this document, and nothing forces them to agree on which element they are looking at.

    The first piece is the signature verifier. It reads the Reference URI="#_abc123", walks the tree to find the element with that id, runs the math, and reports “the signature is valid.” The second piece is the business logic that pulls out the identity. It often does something looser, like “find the first Assertion under Response and read its NameID.” If those two pieces resolve to different elements, you have a problem. The verifier blesses one node. The application trusts a different node. Neither one notices.

    A valid signature only proves that some element in the document was signed. It does not prove that the element you read is that element.

    This is the same family of trust mistake we cover in authentication vs authorization, where proving who someone is gets quietly confused with deciding what they may do. It also rhymes with XXE injection, another case where an XML parser does more, or reads more, than the developer assumed. The XML is trusted as plain data when it is really a set of instructions.

    The wrapping trick at a structural level

    The attacker starts with a real, validly signed assertion captured during their own legitimate login. They cannot forge the signature, and they do not try. Instead they rebuild the document around it.

    The move has two parts. First, take the signed Assertion with id _abc123 and tuck it somewhere the signature verifier will still find it by id, but the business logic will skip. A common hiding spot is inside a wrapper element, or deeper in the tree. Second, inject a brand new Assertion, unsigned, carrying the attacker’s chosen identity, and place it where the business logic looks first.

    The shape of a wrapped document, with placeholder elements, looks like this. The signed original is moved aside. The injected one sits up front.

    <Response>
    
      <!-- injected, UNSIGNED, attacker controlled -->
      <Assertion ID="_evil999">
        <Subject><NameID>admin@acme.example</NameID></Subject>
      </Assertion>
    
      <!-- relocated original, still validly signed -->
      <Wrapper>
        <Assertion ID="_abc123">
          <Subject><NameID>alice@acme.example</NameID></Subject>
          <Signature>
            <Reference URI="#_abc123"/>
            <SignatureValue>...unchanged...</SignatureValue>
          </Signature>
        </Assertion>
      </Wrapper>
    
    </Response>

    Now read it the way each side reads it. The verifier follows URI="#_abc123", finds the relocated original inside Wrapper, checks the math over alice’s assertion, and says “valid.” The business logic asks for the first Assertion under Response, lands on _evil999, and reads admin@acme.example. The result is authentication bypass or full impersonation, with a signature that genuinely validates.

    There are many variants. The signed element can be hidden, duplicated, or nested at a different depth, and the injected element can be placed before, after, or as a sibling, depending on exactly how the consuming code selects its node. The principle behind all of them is the same. XML signature wrapping is a well studied class from academic research, and the original work catalogued a whole tree of these rearrangements. The lesson held up. If the verifier and the consumer can disagree about which element is in play, an attacker will engineer that disagreement.

    Detecting and preventing SAML signature wrapping

    The fix is one idea stated several ways. The element you consume must be exactly the element that was signed. Not an element with the same name. Not the first one you find. The same node, resolved by the same reference the signature used.

    • Bind consumption to the signed node. After the signature verifies, hold a reference to the precise element it covered, and read your identity only from that node. Do not re run a fresh “find the first assertion” query against the document.
    • Reject documents with more than one assertion. A valid login response carries one assertion. If you see two, do not try to pick the right one. Refuse the whole document.
    • Mark and check the signed node. Some libraries let you tag the verified element so later code can assert it is reading the marked node, not a look alike sitting elsewhere in the tree.
    • Avoid id based reference ambiguity. Wrapping leans on the verifier resolving an id to one node while the parser resolves the same name to another. Validate against a strict schema, reject duplicate ids, and do not let two elements answer to the same identifier.
    • Use a hardened, well maintained SAML library. This is not a parser to hand roll. Mature libraries have absorbed years of wrapping reports and apply the position checks for you. Keep them patched.
    • Run schema validation before trusting structure. A schema that forbids stray wrapper elements and extra assertions removes many of the hiding spots wrapping needs.

    For more reading on the trust boundary side of this, see our work under access control. Wrapping is ultimately an access control failure dressed up as a cryptography success.

    Why this slips past review

    The dangerous part of SAML signature wrapping is that the signature check passes. Logs show a valid signature from a trusted issuer. The login works for real users every day. The flaw only appears when someone sends a document built so that the verifier and the consumer look at different elements, and that is a question no one usually writes down. It is exactly the kind of assumption an autonomous researcher that tests assumptions, rather than known payloads, is built to probe, by asking whether “the signature is valid” and “the identity I am using was signed” are truly the same claim. You can read more about our approach on the about page.

    Frequently asked questions

    What is SAML signature wrapping?

    SAML signature wrapping is an attack where an attacker takes a validly signed SAML assertion and rearranges the XML so the signature still validates over the original element while the service provider reads a second, injected assertion that carries the attacker’s chosen identity. The signature is genuinely valid, but the element the application uses is not the element that was signed, which leads to authentication bypass or impersonation.

    Why does a valid signature not stop the attack?

    Because two different pieces of code look at the document. The signature verifier resolves a reference, usually an id, and confirms the math over one element. The business logic separately picks an element to read identity from, often by position or element name. If those two resolve to different nodes, the verifier blesses one assertion while the application trusts another. The signature proves only that some element was signed, not that the element you read is that element.

    How do you prevent SAML signature wrapping?

    Bind consumption to the exact node that was signed, resolving identity only from the element the signature covered rather than re running a fresh search. Reject any response that contains more than one assertion, reject duplicate ids, and validate against a strict schema. Use a hardened, well maintained SAML library instead of hand rolling verification, and keep it patched. See the OWASP SAML Security Cheat Sheet for implementation guidance: https://cheatsheetseries.owasp.org/cheatsheets/SAML_Security_Cheat_Sheet.html

    Is XML signature wrapping a new or theoretical problem?

    No. XML signature wrapping is a well studied class first catalogued in academic research, and it maps to the broader weakness of improper verification of a cryptographic signature, tracked as CWE-347 (https://cwe.mitre.org/data/definitions/347.html). The general lesson, that a verifier and a consumer must agree on exactly which element is in play, applies to SAML and to other signed XML protocols.

  • Dependency Confusion Attack Explained

    Dependency Confusion Attack Explained

    A dependency confusion attack happens when your package manager looks in two places for the same package name, a private registry you control and a public one anyone can publish to, and an attacker plants a package with that exact name on the public side. The resolver sees a higher version number out in public, decides it is newer, and pulls the attacker’s code instead of yours. The install hook then runs on a developer laptop or a build server before anyone reads a line of it.

    How a dependency confusion attack actually works

    Many companies build internal libraries. Say a fictional company, acme.example, keeps a package called acme-billing-utils in a private registry. Developers add it to a manifest and their package manager fetches it. So far nothing is wrong.

    The trouble starts with how the client resolves names. If the same client is also configured to check the public registry, then for any name it cannot find privately, or sometimes for every name, it asks the public registry too. The attacker registers acme-billing-utils on the public registry and gives it version 99.0.0. Your real internal copy is on version 1.4.2. When the resolver compares the two, the public version wins on precedence, and the build pulls the wrong one.

    The attacker never breaks into your registry. They wait outside it, publish a higher version of a name you already trust, and let your own resolver hand them the build.

    Version precedence is the lever

    Package managers treat a higher version as the one you want. That rule is sensible most of the time, since you usually want the newest fix. It turns against you the moment two registries can answer for one name. The attacker does not need to guess your version. They publish something absurd like 99.0.0, and the comparison falls their way every time.

    Install hooks run code, not just copy files

    Installing a package is not only a download. Many ecosystems run a script at install time. In the npm world a postinstall script runs automatically. In Python, code in setup.py executes when the package is built or installed. That script runs with the same rights as the person or process doing the install. On a developer laptop that means access to local files, environment variables, and tokens. On a CI build server it can mean access to deploy keys and signing material. This is the same idea as command injection, since an install hook runs arbitrary commands the moment the package lands.

    A tiny illustrative example

    Here is the shape of the problem, written for defenders. Nothing below is a working payload. It shows how a manifest and a registry view line up so the wrong package gets chosen.

    # package.json on a developer machine at acme.example
    {
      "name": "acme-internal-app",
      "dependencies": {
        "acme-billing-utils": "^1.4.0"
      }
    }
    
    # What the private registry holds
    acme-billing-utils  1.4.2   (your real internal package)
    
    # What the attacker publishes to the PUBLIC registry
    acme-billing-utils  99.0.0  (same name, much higher version)
    
    # A package can declare an install hook that runs automatically
    {
      "name": "acme-billing-utils",
      "version": "99.0.0",
      "scripts": {
        "postinstall": "node ./collect.js"   # runs at install time
      }
    }
    

    The resolver compares 1.4.2 against 99.0.0, picks the higher one, downloads it from the public registry, and runs postinstall. The collection script in this sketch is left empty on purpose. The point is that arbitrary code ran before any review, on whichever machine did the install.

    Where it bites

    Two places take the damage first.

    • CI build servers. These run installs constantly, often with broad permissions and long lived credentials. A build agent that pulls a poisoned package can leak deploy keys, cloud tokens, or source for every project it touches.
    • Developer laptops. A developer running an install brings the attacker’s code onto a machine that holds SSH keys, cloud sessions, and access to internal services. One install can become a foothold inside the network.

    The public research that named this class generically appeared in 2021, when a researcher published internal package names for several organisations to the public registries and watched their builds reach out and run the planted code. We avoid restating specific company names or counts, since the point stands without them, the names were real and the technique worked widely.

    How to detect it

    • Audit which names resolve publicly. List every internal package name, then check whether that name returns anything from the public registry. A private name that resolves in public is a name an attacker can claim.
    • Watch for unexpected high versions. An internal package on 1.4.2 that suddenly offers 99.0.0 from a public source is a clear warning. Diff resolved versions against what your private registry actually serves.
    • Monitor install hooks. Log when postinstall or setup.py code runs during a build, and flag scripts that reach the network or read credentials. A package that never needed a hook before and now ships one deserves a look.
    • Inspect lockfiles. Check the resolved source URL for each dependency. If an internal name resolved against the public registry, the lockfile records it.

    How to prevent it

    • Claim your internal names in public. Reserve every internal package name on the public registries with an empty placeholder you control. An attacker cannot publish a name you already hold.
    • Scope packages to a namespace. Publish internal packages under an organisation scope, for example @acme/billing-utils, and bind that scope to your private registry only. A scoped name will not silently resolve elsewhere.
    • Pin and lock with integrity hashes. Commit a lockfile, pin exact versions, and verify integrity hashes so a swapped artifact fails the check.
    • Point the client at one trusted source. Configure a single trusted registry, or set a per scope registry, so the client never falls back to the public registry for internal names.
    • Disable or vet install scripts. Turn off automatic install scripts where you can, and review the ones you must keep. Many builds run fine with scripts off.
    • Use an internal mirror or proxy. Route every install through a proxy that serves your private packages first and only fetches vetted public ones, so the resolver never faces an open choice between two registries.

    This bug shares a root cause with the rest of the injection and input family, untrusted material crossing into a place that trusts it. The install hook turns a naming gap into command execution, which is why the fix lives in both resolution config and script policy.

    Why this rewards understanding the build, not a payload list

    A dependency confusion attack is not found by firing known payloads at a target. It depends on how one organisation configures its registries, which names live only in private, and whether the client can ever fall back to public. You find it by understanding what the build assumes about where a name resolves, then testing whether that assumption holds.

    That is the kind of assumption an autonomous researcher that tests how an app is meant to work is built to question. We are early and still building, so we make no promises here. If you want to see how that approach reads, our about page explains it.

    Frequently asked questions

    What is a dependency confusion attack?

    It is a software supply chain attack where a package manager checks both a private registry and a public one for the same package name. An attacker publishes a package with your internal name and a higher version number on the public registry. The resolver treats the higher version as newer and pulls the attacker’s code, whose install hook then runs on your developer machines or build servers.

    Why does the attacker’s package get chosen over the real one?

    Package managers treat a higher version as the one you want, which is usually correct. The attacker exploits that rule by publishing an absurd version such as 99.0.0 in public while your real internal package sits on a much lower version. When the client can answer for one name from two registries, the public copy wins on version precedence.

    How does the malicious code actually run?

    Installing a package is not only a download. Many ecosystems run a script at install time, such as an npm postinstall script or code inside a Python setup.py file. That script runs with the same rights as the user or build process doing the install, so it can read tokens, keys, and environment variables before anyone reviews the package.

    How do I prevent a dependency confusion attack?

    Claim your internal package names on the public registries so an attacker cannot register them, scope packages to an organisation namespace bound to your private registry, pin versions and verify integrity hashes in a committed lockfile, point the client at a single trusted registry or per scope registries, disable or vet install scripts, and route installs through an internal mirror. The CWE entry on improper control of code under supply chain compromise covers the wider class at https://cwe.mitre.org/data/definitions/1357.html.

  • JWT Algorithm Confusion Attack Explained

    JWT Algorithm Confusion Attack Explained

    A JSON Web Token carries claims that a server trusts, and a signature that is supposed to prove those claims were not edited. A JWT algorithm confusion attack abuses the one field that decides how that signature gets checked. When a server reads the algorithm name out of the token and obeys it, an attacker can pick an algorithm the server never intended, and forge a token the server accepts as genuine.

    The three parts of a JWT

    A signed JWT is three base64url segments joined by dots: header.payload.signature. The header and payload are JSON. The signature is computed over the first two parts. RFC 7519 defines the token shape, and RFC 7515 (JSON Web Signature) defines how the signature is produced and verified.

    # A typical token for the app acme.example (decoded view, not a real secret)
    header  = {"alg": "RS256", "typ": "JWT"}
    payload = {"sub": "1042", "role": "user", "iss": "acme.example", "exp": 1750800000}
    signature = RSASSA-PKCS1-v1_5( base64url(header) + "." + base64url(payload), private_key )
    
    # On the wire it looks like this (truncated, illustrative only)
    eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDQyIiwicm9sZSI6InVzZXIifQ.SflKx...
    

    The role claim here is what an attacker wants to change from user to admin. The signature is the only thing stopping them. So the whole question becomes: how does the server check that signature, and can the attacker influence the answer.

    Why trusting the header alg is the root flaw

    The alg field in the header tells the verifier which algorithm to use. RS256 means an RSA signature, verified with a public key. HS256 means an HMAC, verified with a shared secret. A careless library reads alg from the token and runs whatever it finds. That hands the attacker control of the verification path. CWE-347, improper verification of a cryptographic signature, is the formal name for the resulting bug class.

    The token is asking the server a question, which key should I be checked with, and the server should never let the token answer it.

    The alg:none variant

    RFC 7518 defines a value of none for the algorithm, meaning the token is unsecured and carries no signature at all. It exists for narrow cases where another layer already provides integrity. The problem is a server that still accepts it on a normal authenticated route.

    An attacker sets the header to {"alg":"none"}, edits the payload to grant themselves an admin role, and sends the token with an empty signature segment:

    # alg:none token (header and payload only, third segment is empty)
    {"alg":"none","typ":"JWT"} . {"sub":"1042","role":"admin"} .
    

    If the server skips signature checking because the algorithm says there is nothing to check, the forged claims sail through. The fix is plain: reject none on any route that requires a signed token.

    The RS256 to HS256 key confusion variant

    This is the sharper version of a JWT algorithm confusion attack, and it works even when the server uses real signatures. Picture acme.example issuing RS256 tokens. The server holds an RSA private key for signing and an RSA public key for verifying. The public key is not a secret. It might sit in a JWKS endpoint, in documentation, or in a mobile app bundle.

    Now the attacker forges a token with the header set to {"alg":"HS256"}. HS256 is symmetric: the same key both signs and verifies. The attacker computes an HMAC over their edited header and payload, using the RSA public key string as the HMAC secret. Then they send it.

    If the server reads alg from the token and switches to HS256, it goes looking for the HMAC secret. In a vulnerable setup it reaches for the only key it has on hand, the RSA public key, and uses that exact string as the secret. It recomputes the HMAC over the same bytes, gets the same value the attacker computed, and the signature matches. The token is accepted.

    The trick rests on one fact. The attacker forged a valid signature using only public information, because in this confused path the verification secret is the public key, and the public key is known to everyone. Auth0 and PortSwigger both documented this pattern, and it remains a common finding in token handling code.

    A short example of the shape

    # What the attacker controls: the header and payload
    header  = {"alg":"HS256","typ":"JWT"}
    payload = {"sub":"1042","role":"admin","iss":"acme.example"}
    
    # The forged signature is an HMAC keyed by the RSA PUBLIC key text
    signature = HMAC_SHA256( signing_input, rsa_public_key_pem )
    
    # Vulnerable server: reads alg=HS256 from the token, verifies HMAC
    #   using the same rsa_public_key_pem it normally uses for RSA verify.
    #   The two HMAC values match, so the forged token is trusted.
    

    No private key was ever needed. The attacker at evil.example only needed the public key text that acme.example was already giving out.

    How to spot it

    • Read the header. Decode a real token and look at alg. If the issuer uses RS256 but the server also accepts HS256 or none on the same route, that mismatch is the warning sign. You can inspect a token’s algorithm and claims with our free JWT security inspector, an in browser tool where nothing you paste leaves the page.
    • Try the swaps in a test environment. Against an app you own, change alg to none with an empty signature, and separately try an HS256 token signed with the published public key. If either is accepted, the server is trusting the header.
    • Audit the verify call. Search the code for the verification function. If it derives the algorithm from the token instead of pinning an expected list, that is the bug in source form.

    How to prevent a JWT algorithm confusion attack

    • Pin the expected algorithm on the server. Pass an explicit allowlist such as ["RS256"] to the verify call. Never derive the algorithm from the incoming token.
    • Reject alg:none. Treat none as invalid on every authenticated route. Do not rely on a library default.
    • Use separate keys per algorithm. A key meant for RSA verification should never be reachable as an HMAC secret. Keep symmetric and asymmetric material in different stores so a confused code path cannot grab the wrong one.
    • Verify before reading claims. Check the signature first and only then read role, sub, or anything else. A token that fails verification should be discarded before any claim is trusted.
    • Keep libraries current. Many JWT libraries hardened these defaults years ago. Older versions still ship the foot guns.

    This bug lives next to broader authorization mistakes, so it is worth reading our access control category for the wider pattern. It also pairs well with understanding authentication vs authorization, since a forged token attacks both at once.

    Why this rewards understanding the app

    You do not find a JWT algorithm confusion attack by replaying a fixed payload. You find it by understanding which algorithm the issuer uses, where the public key is exposed, and whether the verify call pins what it expects. The bug is an assumption, that the token would never lie about how to check itself, and the way to find it is to test that assumption directly.

    That is the kind of bug an autonomous researcher that tests an app’s assumptions is built to surface. You can read more about that approach on our about page.

    Frequently asked questions

    What is a JWT algorithm confusion attack?

    It is an attack where a server reads the alg field out of a JSON Web Token and obeys it, letting the attacker pick how the signature is verified. By choosing an algorithm the server never intended, such as none or HS256 instead of RS256, the attacker forges a token the server accepts. The formal bug class is improper verification of a cryptographic signature, described in MITRE CWE 347.

    How does the RS256 to HS256 key confusion attack work?

    A server that should verify RS256 tokens uses an RSA public key, which is not secret. The attacker forges a token with the header set to {"alg":"HS256"} and computes an HMAC over it using that public key text as the secret. If the server reads alg from the token and switches to HS256, it verifies the HMAC with the same public key, the values match, and the forged token is trusted. No private key is ever needed.

    What is the alg:none bug in JWTs?

    RFC 7518 defines an algorithm value of none for unsecured tokens that carry no signature. The bug is a server that still accepts none on a route requiring a signed token. An attacker sets the header to {"alg":"none"}, edits the payload to grant an admin role, and sends an empty signature segment. A server that skips checking because the algorithm says there is nothing to check will trust the forged claims.

    How do you prevent a JWT algorithm confusion attack?

    Pin an explicit algorithm allowlist such as ["RS256"] on the verify call and never derive the algorithm from the incoming token. Reject none on every authenticated route, keep symmetric and asymmetric keys in separate stores so a confused path cannot use a public key as an HMAC secret, and verify the signature before reading any claim. Keeping JWT libraries current also helps, since many hardened these defaults years ago.

  • The GraphQL Attack Surface: Introspection, Query DoS, Broken Authorization, and Injection

    The GraphQL Attack Surface: Introspection, Query DoS, Broken Authorization, and Injection

    The graphql attack surface comes from a single design choice that makes GraphQL pleasant to build against: the client, not the server, decides the shape of the response. One endpoint at /graphql accepts a typed query, and the caller asks for exactly the fields it wants, as deeply nested as it likes, in whatever batch it cares to assemble. That flexibility is the whole appeal, and it is also the whole problem. A REST API exposes a fixed menu of routes, each returning a fixed payload. A GraphQL API hands the caller a programmable interface to your data graph and trusts them to use it gently. This post walks the specific ways that trust gets abused: how introspection turns the schema into a map, how nested and batched queries turn one HTTP request into thousands of resolver calls, how authorization slips through the gaps between resolvers, how injection still reaches the database, and how to put guards back on each of those.

    What makes the graphql attack surface different from REST

    Start with the model, because every issue below falls out of it. A REST API is a set of endpoints. GET /notes/42 returns a note, POST /notes creates one, and each route is a separate, individually secured thing. The server owns the response shape. If GET /notes/42 does not include the author’s email, the client cannot ask for it; the field simply is not on that route.

    GraphQL collapses all of that into one endpoint and one typed schema. Our invented app, Acme Notes, exposes everything through a single POST to /graphql. The client sends a query describing the exact shape it wants:

    query {
      note(id: 42) {
        title
        author {
          name
          email
        }
      }
    }

    Three things follow from this design, and each one widens the attack surface. First, there is a typed schema that names every type, every field, and every operation, and GraphQL can describe that schema to anyone who asks. Second, the client chooses the shape and depth of the response, so the server cannot reason about one fixed payload; it has to answer whatever query arrives. Third, the work is done by resolvers, one small function per field, each fetching its piece. The query above runs a resolver for note, then for author, then for name and email. The server stitches the result together. That resolver model is elegant and it is exactly where authorization tends to leak, because each resolver is its own little decision point.

    Introspection turns the schema into a map

    GraphQL ships with a reflection system. A special set of meta fields, chiefly __schema and __type, lets a client ask the server to describe itself: every type, every field, every argument, every deprecated operation, and the relationships between them. This is what powers the autocomplete in a GraphQL IDE and the documentation explorer. It is genuinely useful for developers, and it is a reconnaissance goldmine for an attacker.

    A single introspection query returns the full map. The canonical form walks __schema and pulls every type and field:

    query {
      __schema {
        queryType { name }
        mutationType { name }
        types {
          name
          fields(includeDeprecated: true) {
            name
            args { name type { name } }
          }
        }
      }
    }

    Run that against an unguarded endpoint and you learn the entire data model in one request. You see mutations that are not linked anywhere in the UI. You see deprecated fields that still resolve. You see internal types like AdminUser or BillingAccount that the front end never touches but the resolver still serves. There is no guessing at route names the way you would brute force a REST API. The server tells you everything, accurately, because describing itself is a feature.

    What makes this worse than a leaked REST documentation page is precision. Introspection is not a hint or a sample; it is the authoritative description the server uses to validate every query. The argument types it reports are the exact types it enforces. The deprecated fields it lists still resolve, because deprecation in GraphQL is a label, not a removal. An attacker who pulls the schema knows, before sending a single real query, which mutation creates an admin, which field exposes a token, and which argument is an unbounded string. Mapping a REST API is archaeology; mapping a GraphQL API is reading the blueprint the builder left on the table.

    Disabling introspection in production helps but does not fully close the door. Many GraphQL servers, Apollo among them, return field suggestions in error messages: ask for a field that does not exist and the server helpfully replies did you mean, leaking real field names one guess at a time. The tool clairvoyance, by Nikita Stupin, automates exactly this, recovering all or part of a schema from those suggestions even when __schema is turned off. On the testing side, InQL from Doyensec is a Burp Suite extension that parses introspection into ready to send query templates and detects circular references, and graphql-cop by Dolev Farhi is a small auditor that checks whether introspection, suggestions, batching, and depth limits are left open. These are accurate, real tools, and they make the reconnaissance step nearly free. The takeaway is that introspection is a default on convenience, and leaving it on in production means publishing your data model to anyone who points one of these utilities at /graphql.

    Denial of service through nested queries, batching, and aliases

    Because the client controls depth, it controls how much work the server does. The schema is a graph, and graphs have cycles. If a note has an author, and an author has notes, and each note has an author, you can write a query that descends through that relationship as far as you like:

    query {
      note(id: 42) {
        author {
          notes {
            author {
              notes {
                author { name }
              }
            }
          }
        }
      }
    }

    Keep nesting and the resolver count explodes. Each level multiplies the work, and a sufficiently deep circular query can force the server to fetch and join enormous amounts of data from a single small request. The attacker spends a few hundred bytes; the server spends seconds of database time and a heap of memory. This is a denial of service that needs no botnet, just one well shaped query.

    Batching and aliasing amplify it further. GraphQL lets you request the same field many times in one operation by giving each instance an alias. One HTTP request can therefore carry thousands of resolver calls:

    query {
      a: note(id: 1) { title }
      b: note(id: 2) { title }
      c: note(id: 3) { title }
      d: note(id: 4) { title }
    }

    Extend that to thousands of aliases and one request becomes a bulk operation. Many servers also accept an array of operations in a single POST, a separate batching feature with the same effect. Either way, the unit a naive rate limit counts, the HTTP request, no longer matches the unit of work, the resolver call.

    The fix is to stop reasoning about requests and start reasoning about cost. Query depth limiting rejects anything nested past a fixed level, which directly kills the circular query because a cycle has to nest to do damage. Complexity or cost analysis goes further: it assigns a weight to each field, sums the weight of the incoming query before executing it, and refuses queries over a budget. A list field that returns many items costs more than a scalar. A field whose resolver hits the database costs more than one served from memory. By scoring the query statically, the server can decline expensive shapes without ever running them, which is the only way to defend against a query you have not seen before. The OWASP GraphQL Cheat Sheet points at libraries like graphql-cost-analysis and graphql-validation-complexity for exactly this, alongside amount limits on list fields, server side timeouts as a backstop for anything that slips through, and a DataLoader to batch the resolver’s own database calls so legitimate nesting does not fan out into a query per node. The principle is to bound the work a single query is allowed to demand, regardless of how clever its shape is.

    Broken authorization at the field and object level

    This is where GraphQL hurts the most, and it follows directly from the resolver model. In a REST API the authorization check usually lives at the route: a middleware in front of GET /admin/users decides who gets in, and everything behind that one door is covered. In GraphQL there is no route to guard. There is one endpoint and a tree of resolvers, and each resolver is responsible for its own access control. Authorization is not enforced at the door; it is enforced at every field, and it only takes one unguarded field to leak.

    Picture Acme Notes. The note resolver carefully checks that the caller owns the note before returning it. Good. But a note has an author, and the author type exposes email and phone, and the resolver for author was written assuming you only ever reach it through your own notes. An attacker reaches it through a shared note, or through a different relation entirely, and now reads contact details for users they have no business seeing. The guarded object was the note; the unguarded one was reached by traversing a nested relation off it. That is broken object level authorization, the same class the OWASP API Security Top 10 ranks first as API1:2023, and the same bug the web calls IDOR. GraphQL makes it especially easy to introduce because the relationships that let a client walk from one object to another are the entire point of the data graph.

    In REST you guard the doors. In GraphQL there are no doors, only a graph, and every node has to guard itself. Miss one and an attacker walks in through a neighbor.

    There is a second flavor of this that introspection sets up directly. Because the schema lists every type and every argument, an attacker can call an object by its identifier even when the UI never offers it. Suppose Acme Notes hides delisted notes from every listing, but the note(id:) field still resolves any id it is given. The listing is a presentation choice; the resolver is the real access boundary, and if the resolver only checks that the id is well formed rather than that the caller owns it, the hidden object is one direct query away. The fix and the failure are the same as above: the check has to live in the resolver, on the object, not in the layer that decided what to show.

    The defense is per resolver authorization treated as a first class concern, not a sprinkle. Every resolver that returns sensitive data checks the caller’s right to that specific object, on both the nodes and the edges of the schema as the cheat sheet puts it. Centralizing this logic, rather than hand writing a check in each resolver, is what keeps one forgotten field from undoing the rest, and it is why teams move the decision into a shared authorization layer that every resolver consults rather than trusting each author to remember. If you want the broader pattern behind this bug, see our note on broken object level authorization and IDOR.

    Injection still reaches the database through resolver arguments

    GraphQL’s type system validates the shape of a query, not the safety of its values. A field that takes a String argument will reject a number, but it will happily pass an attacker controlled string straight through to whatever the resolver does next. If that resolver interpolates the argument into a database query, a shell command, or a NoSQL filter, you have the same injection you would have anywhere else, just arriving over GraphQL.

    Suppose Acme Notes has a search field:

    query {
      searchNotes(filter: "Marketing") {
        title
      }
    }

    If the searchNotes resolver builds its SQL by concatenating that filter string, an attacker sends a filter value crafted to break out of the string and the database executes it. The typed schema gave a false sense of safety here, because the type checked that filter is a string, not that the string is harmless. The fix is the ordinary one: parameterized queries and strict input validation inside the resolver, using GraphQL’s own scalars and enums to constrain arguments where you can, and never trusting an argument just because it passed type checking. The OWASP GraphQL Cheat Sheet is explicit that the type system is not an input validation layer.

    Batching attacks that bypass rate limits on sensitive mutations

    The aliasing trick from the denial of service section has a sharper edge when it is pointed at authentication. Rate limits on a login or a two factor check almost always count HTTP requests: five attempts a minute from this IP, then a lockout. Aliases let an attacker pack many attempts into one request, and if the limiter counts requests rather than operations, the limit never trips.

    mutation {
      a: login(user: "victim", code: "0000") { token }
      b: login(user: "victim", code: "0001") { token }
      c: login(user: "victim", code: "0002") { token }
      d: login(user: "victim", code: "0003") { token }
    }

    Stack thousands of those aliases and a single request brute forces a four digit two factor code, or sprays a password list against a login mutation, all under one entry in the rate limiter’s ledger. The same applies to coupon redemption, password reset codes, and any mutation whose protection assumed one guess per request. PortSwigger documents this alias based rate limit bypass in detail, and it is one of the first things a GraphQL specific scanner checks for.

    The defenses here are pointed. Count operations, not requests, so the limiter sees each aliased login as a separate attempt. Better yet, disable batching and aliasing on sensitive mutations entirely, or cap the number of aliases for a single field, so a login can appear once per request. The cheat sheet’s guidance is to prevent batching for sensitive objects like authentication and to enforce per object request rate limiting in code rather than only at the HTTP layer.

    The defenses, gathered in one place

    None of these issues is exotic, and the controls map cleanly onto them. Treat this as the checklist:

    • Restrict introspection in production. Disable __schema on public deployments, and turn off field suggestions too, since tools like clairvoyance rebuild the schema from suggestion errors alone. Keep introspection on only in environments you control.
    • Limit query depth and total cost. Reject queries nested past a fixed depth, and run cost analysis that weights each field and refuses anything over a budget before execution. Add amount limits on list fields and a server side timeout as backstops.
    • Use persisted queries or an allowlist. Register the exact queries your clients are allowed to send and reject everything else. An arbitrary query interface becomes a fixed, known set, which kills introspection, ad hoc nesting, and most batching abuse in one move.
    • Enforce authorization in every resolver. Check the caller’s right to each object on both nodes and edges, centralize the logic so it cannot be forgotten, and assume any field can be reached through a nested relation, not just through its obvious parent.
    • Validate arguments and parameterize. Never trust a value because it passed type checking. Parameterize database queries, validate inside the resolver, and constrain arguments with scalars and enums.
    • Disable batching where it bypasses rate limits. Count operations rather than requests, cap aliases per field, and turn off batching for authentication and other sensitive mutations.

    For the canonical references, anchor on the OWASP API Security Top 10, which frames the authorization and rate limiting risks; the OWASP GraphQL Cheat Sheet, which gives the concrete server side controls; and PortSwigger’s GraphQL API vulnerabilities material, which walks the attacks hands on. For tooling, graphql-cop, InQL, and clairvoyance are the real, current utilities worth knowing.

    The assumption that breaks

    Step back from the individual bugs and one assumption is holding all of them up. GraphQL hands the client control over the shape of the response, the depth it descends to, and the volume of work a single request demands, and it assumes the client is not hostile. Every issue in this post is that assumption failing. Introspection assumes you only want the schema to build against it, not to map it for an attack. Nesting assumes you ask for what you need, not for a circular query that melts the database. Aliasing assumes you batch for convenience, not to brute force a login under one rate limit entry. The resolver model assumes each field is reached through a friendly path, not traversed from an unexpected neighbor.

    That is what makes the graphql attack surface its own thing rather than REST with extra steps. The flexibility that makes GraphQL a good developer experience is precisely the flexibility an attacker uses, and the only durable fix is to bound what the client is allowed to ask for: restrict the schema’s visibility, cap the cost of a query, allowlist the operations, and check authorization at every node. The gap here is not a single broken function. It is the distance between what the server assumes a client will do and what a client can actually arrange, and that gap is the kind of thing you find by asking what each component trusts and why, rather than by scanning for a known bad string. It is exactly the kind of assumption an autonomous researcher built to test assumptions is meant to surface. Learn more about that approach on our about page.

    Frequently asked questions

    What makes the GraphQL attack surface different from a REST API?

    A REST API exposes fixed routes that each return a fixed payload, so the server owns the response shape. GraphQL exposes one endpoint and a typed schema, and the client chooses which fields it wants, how deeply nested, and in what batch. That flexibility means the server has to answer whatever query arrives, which opens introspection recon, query based denial of service, and field level authorization gaps. PortSwigger walks these attacks hands on in its GraphQL API vulnerabilities material.

    Why is GraphQL introspection a security concern?

    Introspection is a built in reflection system. A single query against __schema returns every type, field, argument, deprecated operation, and hidden mutation, handing an attacker a full map of your data model in one request. Disabling it in production helps, but servers that return field suggestions in errors still leak field names, and the tool clairvoyance rebuilds the schema from those suggestions alone. The OWASP GraphQL Cheat Sheet recommends disabling introspection and suggestions on public deployments.

    How do batching and aliasing bypass rate limits on a login mutation?

    Rate limits usually count HTTP requests, but GraphQL aliases let one request carry many copies of the same field. An attacker can pack thousands of aliased login or two factor attempts into a single request, and a limiter counting requests never trips. The fix is to count operations rather than requests, cap aliases per field, and disable batching for sensitive mutations. This maps to the rate limiting risks in the OWASP API Security Top 10.

    Why is broken authorization so common in GraphQL?

    GraphQL has no route to guard. There is one endpoint and a tree of resolvers, and each resolver enforces its own access control, so it only takes one unguarded field, often reached through a nested relation, to leak data. This is broken object level authorization, ranked first in the OWASP API Security Top 10 as API1:2023. The fix is per resolver checks on both nodes and edges, centralized so a single field cannot be forgotten.

  • What Is a Padding Oracle Attack and How It Decrypts CBC Without the Key

    What Is a Padding Oracle Attack and How It Decrypts CBC Without the Key

    A padding oracle attack lets someone decrypt CBC encrypted data without ever knowing the key, using nothing but a single bit of feedback the system was never supposed to give away. The attacker submits a ciphertext, the system tries to decrypt it, and the system tells the sender one thing it should have kept to itself: whether the padding came out valid. That one bit, asked over and over against tweaked ciphertext, is enough to peel the plaintext apart one byte at a time, and even to forge ciphertext that decrypts to a message the attacker chose. The leak does not have to be an explicit error. A status code, a timing difference, or a connection that drops a hair faster is the same bit by another name. This post walks the mechanism from the ground up: how CBC chains its blocks, why messages get padded, where the oracle hides, the byte at a time math that turns it into a full decryption, and the real attacks that took this from a 2002 paper to a protocol wide emergency.

    What a padding oracle attack actually is

    A padding oracle attack is a chosen ciphertext attack against a block cipher running in CBC mode. The target is not the cipher itself. AES is not broken here, and neither is the key. The target is a small piece of behavior wrapped around the cipher: the part that, after decrypting, checks whether the padding bytes at the end of the message are well formed and reacts differently when they are not. An oracle, in the cryptographic sense, is any function an attacker can query that answers a yes or no question about a secret. Here the question is just is this padding valid, and the answer, leaked through any side channel at all, is the lever that pries the whole message open.

    To see how a yes or no about padding becomes a full decryption, you have to look at two pieces working together: how block ciphers pad messages, and how CBC mode chains its blocks. Neither is dangerous alone. The danger is in the seam between them.

    CBC mode and why padding exists

    A block cipher encrypts a fixed size chunk at a time. AES works on 16 byte blocks and nothing else. Feed it 16 bytes, get 16 bytes back. But real messages are not tidy multiples of 16. A session cookie might be 30 bytes, a form field 7 bytes. Something has to stretch the message out to a whole number of blocks before the cipher can touch it, and that something is padding.

    The most common scheme is PKCS#7. The rule is simple and self describing: figure out how many bytes you need to reach the next block boundary, call it N, and append N bytes each holding the value N. Need 4 bytes to fill the block, you append 04 04 04 04. Need 1 byte, you append a single 0x01. If the message already lands exactly on a boundary, you add a whole extra block of 16 16 16 ... 16 so that there is always padding to strip and the receiver is never guessing. On the way back out, the receiver reads the value of the final byte, say it is N, checks that the last N bytes all equal N, and lops them off. If those bytes do not form a valid pattern, the padding is wrong, and the receiver knows the message was malformed.

    That validity check is the seed of the whole problem. It is a test the receiver runs on attacker supplied bytes, and it has exactly two outcomes.

    How CBC chains the blocks

    CBC stands for cipher block chaining, and the chaining is the part that matters. You cannot just encrypt each block on its own, because identical plaintext blocks would produce identical ciphertext blocks and leak the structure of the message. CBC fixes this by mixing each plaintext block with the ciphertext of the block before it. If you are still building intuition for how plaintext, ciphertext, and XOR relate before tackling a modern mode like CBC, our free classical cipher solver lets you experiment with substitution ciphers and common encodings by hand, a learning aid for the basics rather than anything that touches the attack below.

    Encryption walks the blocks in order. Before a plaintext block P[i] is handed to the cipher, it is XORed with the previous ciphertext block C[i-1]. The very first block has no predecessor, so it is XORed with a random initialization vector, the IV, which travels alongside the ciphertext. In symbols:

    C[i] = AES_encrypt( P[i] XOR C[i-1] )
    P[i] = AES_decrypt( C[i] ) XOR C[i-1]

    The second line is where the attack lives, so it is worth slowing down. To recover a plaintext block on decryption, the receiver runs the ciphertext block C[i] through the cipher’s decrypt function, producing an intermediate value, and then XORs that intermediate value with the previous ciphertext block C[i-1]. Call the intermediate value I[i], so that I[i] = AES_decrypt( C[i] ) and the plaintext is simply P[i] = I[i] XOR C[i-1].

    Here is the crucial fact. The intermediate value I[i] depends only on C[i] and the key. It does not depend on C[i-1] at all. If the attacker changes the previous ciphertext block, the cipher still produces the exact same I[i], and the only thing that changes is the XOR applied to it afterward. The attacker controls C[i-1] completely, because it is just data in the ciphertext they are submitting. So the attacker holds one side of the final XOR in their hand. They are one unknown away from the plaintext, and that unknown is I[i].

    The leak: one bit that should never escape

    Put the two pieces together. The attacker takes a ciphertext block C[i] they want to decrypt, and they prepend a block of bytes they fully control, which the receiver will treat as the previous ciphertext block. The receiver decrypts C[i] to the fixed intermediate I[i], XORs it with the attacker’s chosen block to get some plaintext, and then checks the padding of that plaintext. Because the attacker is choosing the previous block byte by byte, they are choosing the output of that final XOR byte by byte, which means they are steering the plaintext the padding check sees.

    The receiver then does the one thing it must not do: it reveals whether the padding was valid. Maybe it returns a BAD_PADDING error distinct from a BAD_MAC error. Maybe both return the same error text but the padding failure comes back a few microseconds sooner because the code bails out before computing a MAC. Maybe a web app returns HTTP 500 on a decryption fault and HTTP 200 on a logic error further down. Any observable difference between valid and invalid padding is the oracle. The attacker does not need the plaintext spelled out. They need the system to answer one yes or no question about ciphertext they crafted, and answer it reliably.

    The cipher was never broken. The key never leaked. The system was simply willing to answer, thousands of times, a single question it believed was harmless: did this decrypt to something with valid padding?

    The byte at a time decryption

    Now the math. The goal is to recover the last byte of the intermediate value I[i], because once every byte of I[i] is known, the real plaintext falls out by XORing I[i] with the genuine previous ciphertext block. Knowing I[i] is knowing the plaintext.

    The attacker works on the last byte first. They take their controllable previous block, call it C', and they set its last byte to a guess value g, running g through all 256 possibilities from 0x00 to 0xFF. For each guess they submit C' followed by C[i] to the oracle and watch the answer. The decrypted last plaintext byte that the padding check sees is:

    P_last = I_last XOR g

    For almost every value of g the padding is invalid and the oracle says no. But there is a value of g for which the last plaintext byte comes out to 0x01, and a final byte of 0x01 is, by itself, valid PKCS#7 padding: it claims a single byte of padding whose value is one. When that happens the oracle says yes. At that moment the attacker knows:

    I_last XOR g = 0x01
    therefore  I_last = g XOR 0x01

    The last byte of the intermediate value is recovered with at most 256 queries, and no key was involved. There is one wrinkle worth naming: occasionally a yes is a false positive, where the byte before the last happened to make the plaintext end in 02 01 or similar, which is also valid. The attacker resolves it by perturbing the second to last byte of C' and re testing; if the padding still validates, the last byte really was forced to 0x01.

    Walking right to left across the block

    With I_last in hand, the attacker moves to the second to last byte, and the trick is to aim for padding of length two. They want the decrypted block to end in 02 02. They already know I_last, so they can set the last byte of C' to force the final plaintext byte to 0x02 exactly, using C'_last = I_last XOR 0x02. Then they brute force the second to last byte of C' through all 256 values until the oracle reports valid padding, which now means the block ends in the valid two byte pattern 02 02. That reveals the second to last byte of I[i] by the same XOR relation, I_second = g XOR 0x02.

    The pattern repeats leftward. To recover the byte at position k, the attacker fixes every already known byte to the right so the tail decrypts to the padding value k_pad repeated, then brute forces position k until the padding validates. Each byte costs at most 256 oracle queries, so a 16 byte block costs at most 16 times 256, roughly 4096 questions, to recover in full. Repeat per block and the entire message is decrypted. The whole thing runs on one fact: P[i] = AES_decrypt(C[i]) XOR C[i-1], with the attacker owning C[i-1] and the oracle confirming when the right side lands on valid padding.

    Notice what the attacker never needs. They never see the key, never run the cipher in the forward direction, and never have to guess more than 256 values at any step. The work is linear in the length of the message, not exponential, which is what separates this from brute force and makes it genuinely practical. Picture our invented app, Acme Notes, storing a session as an encrypted cookie and returning a clean error whenever a cookie fails to decrypt into well formed data. An attacker with a stolen cookie they cannot read, but can replay with edits, now has a live oracle: each tweaked cookie comes back valid or invalid, and a few thousand requests later the plaintext session, user id and all, is sitting in front of them. No alarm fires, because every individual request looks like an ordinary client sending a slightly malformed cookie.

    Turning the oracle into an encryption machine

    The same lever runs in reverse, which surprises people the first time they see it. Once the attacker can recover the intermediate value I[i] for any chosen ciphertext block C[i], they can forge ciphertext that decrypts to any plaintext they want. They pick a plaintext block P_target. They run the padding oracle against an arbitrary C[i] to learn its intermediate I[i]. Then they simply set the previous block to C[i-1] = I[i] XOR P_target, because AES_decrypt(C[i]) XOR C[i-1] = I[i] XOR (I[i] XOR P_target) = P_target. Chaining this construction block by block, working from the last block backward and choosing a fresh C[i] at each step, lets the attacker build an entire ciphertext that decrypts to a message of their choosing, all without the key. A pure decryption oracle has become a forgery tool. Vaudenay’s original paper laid out exactly this reversal.

    POODLE and Lucky Thirteen: the oracle in the wild

    This is not a chalkboard curiosity. Serge Vaudenay published the attack in 2002 in a paper titled Security Flaws Induced by CBC Padding, applying it to SSL, IPSEC, and WTLS. For years it was treated as a known issue with known mitigations. Then two attacks proved the mitigations were leakier than anyone wanted to admit.

    POODLE: CVE-2014-3566

    POODLE, disclosed in October 2014 and tracked as CVE-2014-3566, stands for Padding Oracle On Downgraded Legacy Encryption. The flaw lives in SSLv3, an obsolete protocol that almost everything still supported as a fallback. In SSLv3’s CBC mode, the padding bytes are not fully specified and not covered by the message authentication code. The receiver checks the length byte of the padding but does not verify the padding content, which is precisely the validity gap a padding oracle needs. A man in the middle who can force a connection to roll back from TLS to SSLv3, then make the victim resend the same secret over and over across fresh connections, can recover a chosen byte of ciphertext such as a session cookie in around 256 requests per byte. The downgrade is the clever part: even a client and server that both prefer modern TLS can be shoved back onto the vulnerable SSLv3, which is why the fix was not patching SSLv3 but ripping it out entirely.

    Lucky Thirteen: the timing variant

    Lucky Thirteen, disclosed in 2013 by Nadhem AlFardan and Kenneth Paterson and tracked as CVE-2013-0169, showed that you do not even need an explicit error to build the oracle. TLS implementations had been hardened so that bad padding and bad MAC returned the same error, closing the obvious leak. But the time taken to process a record still depended on the padding, because the amount of data fed into the MAC computation changed with how many bytes the code believed were padding. That tiny timing difference, measured carefully across many sessions, was itself the oracle. The name comes from the 13 byte TLS header that shaped the timing arithmetic. Lucky Thirteen made the point that a side channel does not have to be a message at all. A consistent difference in how long something takes is information, and information about padding validity is a padding oracle.

    It is worth placing this alongside its neighbors. A padding oracle is not insecure deserialization, where untrusted bytes become live objects, and it is not a network level fingerprinting trick. But all three share a shape: a component reveals more about how it processed input than it meant to, and an attacker turns that excess into leverage. Here the excess is a single bit about padding, and the leverage is total.

    The fix: authenticate before you decrypt

    The root cause is that the system makes a decision based on decrypted bytes before it has checked that those bytes are authentic. The padding check runs on ciphertext the attacker forged, and the result of that check escapes. Every fix is a variation on closing that ordering.

    The classic construction is encrypt then MAC. After encrypting the plaintext, you compute a message authentication code over the ciphertext, and you append it. On the way back in, you verify the MAC first, over the raw ciphertext, before you decrypt anything or look at any padding. If the MAC does not match, the ciphertext was tampered with, and you reject it immediately, having revealed nothing about padding because you never got that far. The attacker’s forged ciphertext fails the MAC check, the padding check never runs, and there is no oracle to query. The order is the whole point: the authentication has to gate the decryption, not the other way around.

    The modern answer folds both jobs into a single primitive: authenticated encryption, most commonly AES-GCM. An AEAD cipher encrypts and authenticates in one operation, so there is no separate padding check to leak and no separate MAC step to misorder. AES-GCM is also a stream style construction that needs no block padding at all, which removes the padding oracle’s target outright. The practical lesson the whole saga taught the field is short: do not compose your own encrypt and authenticate steps, and do not run a plain CBC cipher with a bolt on MAC unless you have proven the ordering and the constant time behavior. Reach for an AEAD mode and let it do both jobs together. The Vaudenay paper that started it all, and the Cryptopals CBC padding oracle challenge that lets you build one by hand, are both worth working through if you want the mechanism in your fingers rather than just your notes.

    The assumption that breaks

    Strip away the blocks and the XORs and one assumption is left holding the whole thing up. The system assumes that telling the sender whether the padding was valid is harmless. It feels harmless. Padding is plumbing, a formatting detail, the sort of thing you would happily log or return in an error so a developer can debug a malformed request. Surely a yes or no about formatting gives nothing away. But that single bit, asked enough times against ciphertext the attacker controls, is a decryption oracle and a forgery oracle at once. The harmless answer is the entire attack.

    The bug is not in AES and not in CBC. It is in a trust boundary drawn one step too late, where a check ran on unauthenticated bytes and its result was allowed to escape. That gap between what a system assumes it is safely revealing and what an attacker can actually reconstruct from it is the kind of flaw you find by asking, of every response a system gives, what does this answer tell someone who is asking it ten thousand times on purpose. It is exactly the kind of assumption an autonomous researcher built to test assumptions is meant to catch: not a known bad string to grep for, but a quiet belief that a side channel was too small to matter. Authenticate before you decrypt, reach for AES-GCM, and treat every difference a system can show, in errors, in status codes, in timing, as something an attacker is already measuring. Learn more about that approach on our about page.

    Frequently asked questions

    What is a padding oracle attack in simple terms?

    It is a way to decrypt CBC encrypted data without the key by abusing a system that reveals whether the padding of a decrypted message was valid. The attacker submits altered ciphertext, watches whether the padding check passes or fails, and uses that single yes or no answer to recover the plaintext one byte at a time. The cipher and the key stay intact; only the surrounding validity check leaks. Serge Vaudenay described the original attack in his 2002 paper Security Flaws Induced by CBC Padding.

    How does flipping bytes in the previous ciphertext block recover plaintext?

    In CBC mode the plaintext is P[i] = AES_decrypt(C[i]) XOR C[i-1], and the intermediate value AES_decrypt(C[i]) depends only on the key, not on the previous block. Because the attacker fully controls the previous block, they can brute force its last byte through all 256 values until the oracle reports valid padding, which forces the final plaintext byte to 0x01 and reveals the intermediate byte by XOR. Repeating right to left recovers the whole block. The Cryptopals CBC padding oracle challenge walks the math hands on.

    What was the POODLE vulnerability?

    POODLE, tracked as CVE-2014-3566 and disclosed in October 2014, stands for Padding Oracle On Downgraded Legacy Encryption. It exploits SSLv3, whose CBC padding is not covered by the message authentication code, giving an attacker a padding oracle. A man in the middle forces a connection to roll back from TLS to SSLv3, then recovers a chosen ciphertext byte such as a session cookie in around 256 requests. The fix was to disable SSLv3 entirely, as described in the Oracle POODLE advisory.

    How do you prevent a padding oracle attack?

    Authenticate before you decrypt. Use encrypt then MAC so the message authentication code is verified over the ciphertext before any padding is checked, which means forged ciphertext is rejected before the padding check ever runs. Better still, use an authenticated encryption mode such as AES-GCM, which combines encryption and authentication in one primitive and needs no block padding to leak. The timing variant Lucky Thirteen showed that even matching error messages leak through timing, so constant time processing matters too.

  • How NTLM Relay Works and Why a Portable Authentication Breaks Active Directory

    How NTLM Relay Works and Why a Portable Authentication Breaks Active Directory

    An ntlm relay attack works because NTLM proves you know a password to one server but never ties that proof to the server you meant to reach. A machine authenticates to a host the attacker controls, and the attacker forwards that authentication, byte for byte, to a completely different server, where it lands as a valid login from the victim. Nothing is cracked. No password crosses the wire in either direction. The attacker is a relay sitting in the middle, taking an authentication that was meant for them and spending it somewhere it was never meant to go. This post walks the whole mechanism one step at a time: how the three message NTLM handshake actually works, why the proof it produces is portable to the wrong destination, how an attacker gets a victim to authenticate in the first place, where they relay it, and how each defense closes a different part of the gap.

    What an ntlm relay actually is

    NTLM is the older challenge response authentication protocol that Windows still falls back to across an Active Directory network, especially when a client reaches a server by IP address or by a name Kerberos cannot resolve to a service principal. It is a question and answer ritual. The server asks a hard question only someone who knows the password could answer, and the client answers it without ever stating the password. That property, no password on the wire, is genuinely good. The problem is everything the protocol forgets to check around it.

    The core flaw is a missing binding. When a client proves it knows a secret, that proof does not say which server it was meant for. It does not name the channel it traveled over. It is a free floating token of authentication that any server will accept as long as the math checks out. So a man in the middle who receives one valid authentication can carry it, unchanged, to a different server and be treated as the victim there. That relayed identity is the whole attack, and on a domain it routinely escalates from one captured login to full control of Active Directory.

    It helps to be precise about what this attack is not, because the name invites confusion. It is not a pass the hash attack, where the attacker already holds a stolen password hash and replays it. In a relay the attacker never possesses the hash at all; they only move a one time signed answer between two parties. It is not a brute force or a crack, because nothing offline happens to the response. And it is not a Kerberos attack, because Kerberos tickets are scoped to a named service and resist this kind of redirection by design. NTLM relay is its own thing: a live, in the moment forwarding of a genuine authentication to an unintended destination, exploiting a gap that lives in the protocol rather than in any one machine’s configuration.

    The NTLM handshake, message by message

    NTLM authenticates a client to a server in three messages. The Windows internals and most tooling call them Type 1, Type 2, and Type 3, but they map cleanly onto NEGOTIATE, CHALLENGE, and AUTHENTICATE. Picture a workstation in the Acme domain, acme.local, connecting to a file server.

    NEGOTIATE, the opening offer

    The client opens by sending a NEGOTIATE message. This is the Type 1 packet. It announces that the client wants to authenticate with NTLM and lists the options it supports, things like which NTLM version and which cryptographic flags it can handle. It carries no proof of identity yet. It is the client saying, here is how I would like to do this, what is your challenge.

    CHALLENGE, the hard question

    The server answers with a CHALLENGE message, the Type 2 packet. The important content is a randomly generated eight byte number called the server challenge. The server makes up a fresh random value every time and sends it down. The point of the randomness is that the answer to last time’s challenge is useless this time, which is meant to stop a simple replay of a recorded response. The server keeps a copy of the challenge it just sent so it can check the answer.

    AUTHENTICATE, the proof

    Now the client proves itself. It takes the server challenge and combines it with the cryptographic hash derived from the user’s password, which the client already holds, and with NTLMv2 it mixes in its own client challenge and a timestamp as well. It runs that combination through a keyed hash, HMAC-MD5 with the password derived key, and the output is the challenge response. That response goes back inside the AUTHENTICATE message, the Type 3 packet, alongside the username and domain.

    Here is the elegant part and the dangerous part at once. The password never travels. The client demonstrates that it holds the password hash by signing the server’s specific random challenge with it. The server, which can compute the same answer because it can ask a domain controller to validate the response against the stored hash, checks whether the client’s answer matches. If it does, the client has proven knowledge of the secret without ever transmitting the secret. Authentication succeeds.

    Why the proof is portable to the wrong server

    Walk back through what the client actually signed. It signed the server’s eight byte challenge. It did not sign the hostname it was connecting to. It did not sign the IP address, the service, or the network channel underneath. By default the AUTHENTICATE message contains a proof of password knowledge that is bound to a random number and to nothing else about the destination.

    So put an attacker in the middle. The victim machine starts authenticating to a server the attacker controls, call it the rogue endpoint. The attacker does not answer as a normal server. Instead the attacker opens its own NTLM connection to a real target server somewhere else on the network, a domain controller, say. The real target sends back its own CHALLENGE. The attacker takes that challenge and passes it straight back to the victim as if it were the rogue endpoint’s own challenge. The victim dutifully signs it with its password hash and returns the AUTHENTICATE message. The attacker forwards that AUTHENTICATE message, verbatim, to the real target. The target validates it, sees a correct answer to the exact challenge it issued, and grants the attacker a fully authenticated session in the victim’s name.

    The attacker never learns the password and never cracks a hash. They are a courier, carrying one server’s question to the victim and carrying the victim’s answer to a different server, and both ends believe they are talking to who they expected.

    This is why it is called a relay rather than a crack. The authentication is genuine. It is simply spent against a server the victim never intended to reach. Everything downstream is built on that single substitution.

    One detail matters for understanding the defenses later. The reason the substitution succeeds is that the victim’s signed response is computed over the challenge the attacker handed it, and that challenge is the real target’s challenge. The attacker is not generating challenges of their own; they are a conduit passing the target’s question through to the victim. That is what keeps the math consistent at the far end. It also explains why any defense that gives the victim a way to notice it is signing for the wrong server, or that ties the response to something the attacker cannot also forward, breaks the relay cleanly. The attacker controls the routing but not the content, and the content is where the cure lives.

    Step one for the attacker, getting an authentication to relay

    A relay needs an inbound authentication to forward. The attacker has two broad ways to make one appear: wait for it by poisoning name resolution, or force it by coercing a machine to authenticate on demand.

    Poisoning name resolution

    Windows networks are chatty and trusting about names. When a machine cannot resolve a name through DNS, it falls back to broadcast protocols that ask the whole local segment, who is this. LLMNR, NBT-NS, and mDNS are exactly that fallback. They are unauthenticated broadcasts, so any machine on the segment can answer. A user fat fingers a share name, or an application looks up a host that no longer exists, and the broadcast goes out asking the network to identify it.

    The tool Responder listens for those broadcasts and answers all of them, claiming to be whatever name was requested. The victim believes it found the host, connects, and begins authenticating with NTLM to the attacker’s machine. That is the inbound authentication the relay needs, harvested passively just by answering questions nobody was authorized to answer. The attacker does not have to provoke anything; on a busy network these mistyped names and stale lookups happen on their own throughout the day, and Responder simply scoops up whatever wanders by. The quality of the catch is a matter of patience and luck, which is why poisoning is often the opening move rather than the finishing one.

    Coercing authentication on demand

    Waiting is unreliable, so attackers prefer to compel a specific machine, ideally a high value one like a domain controller, to authenticate to them whenever they like. Several Windows protocols can be tricked into making an outbound authenticated connection to an attacker chosen host.

    The best known is PetitPotam, which abuses the Encrypting File System Remote Protocol, MS-EFSRPC. Discovered by Gilles Lionel, it is tracked as CVE-2021-36942, a Windows LSA spoofing vulnerability that Microsoft addressed in its August 2021 updates. An attacker calls an MS-EFSRPC method such as EfsRpcOpenFileRaw against a target and supplies an attacker controlled path. The target, including a domain controller, then reaches out and authenticates to that path over NTLM using its powerful machine account. The original PetitPotam variant could be triggered without authentication, which is what made it so sharp.

    It is one of a family. The PrinterBug, exploited by the SpoolSample technique, abuses the Print System Remote Protocol to make a machine’s spooler authenticate to an attacker host. PrivExchange abused a Microsoft Exchange feature to make the Exchange server authenticate with its highly privileged account. Different doors, same result: a chosen, often privileged machine account hands the attacker an NTLM authentication ready to relay.

    Step two, relaying it with ntlmrelayx

    Capturing the authentication is only half. The other half is forwarding it to a useful target before it expires, and the standard tool for that is ntlmrelayx, an example script in the Impacket toolkit. It is often paired with Responder or a coercion trigger: one component produces the inbound NTLM authentication, ntlmrelayx forwards it to a target server and then does something with the authenticated session. Where it points decides the outcome.

    Relay to LDAP, granting RBCD

    If the relay target is a domain controller’s LDAP service and the relayed identity has the rights, ntlmrelayx can write to Active Directory as the victim. A favored move is configuring resource based constrained delegation, RBCD. The attacker writes the msDS-AllowedToActOnBehalfOfOtherIdentity attribute on a victim computer object so that an account the attacker controls is allowed to impersonate any user to that computer. With RBCD in place the attacker can later request Kerberos tickets impersonating a domain admin to the victim machine and take it over. The relay grants the delegation; the delegation grants the takeover.

    Relay to SMB, reading and running

    Relayed to the SMB service on a target where the victim is a local administrator, the authenticated session lets the attacker act as an admin on that host: dump the local secrets, read the SAM, or execute commands. This is the classic relay outcome and the reason SMB signing exists.

    Relay to AD CS HTTP enrollment, the ESC8 path

    The most damaging target is Active Directory Certificate Services. Many AD CS deployments expose a web enrollment endpoint over plain HTTP that accepts NTLM authentication. The SpecterOps research that catalogued AD CS abuses, the paper titled Certified Pre-Owned, named this relay scenario ESC8. The attacker coerces a domain controller with PetitPotam, then relays the DC’s machine account authentication with ntlmrelayx to that AD CS web enrollment endpoint and requests a certificate for the domain controller. AD CS issues one. Now the attacker holds a certificate that authenticates as the domain controller. They use it to request a Kerberos ticket as the DC, and from there they can perform a directory replication and dump every credential in the domain. One coerced authentication becomes full domain compromise.

    What makes this chain so potent is how little the attacker needs to start it and how durable the prize is. The PetitPotam trigger could fire without any prior foothold in its original form, so an unauthenticated attacker on the network could begin the whole sequence. And a certificate is not a session that times out in minutes; it is a credential the attacker can hold and reuse for as long as it remains valid, surviving password resets of the account it impersonates. That combination, a low cost trigger feeding a long lived credential for the most privileged account in the domain, is why the PetitPotam to ESC8 path drew so much attention and so many emergency patches. It compresses the distance from outsider to domain owner into a handful of network calls, none of which involve guessing or cracking a single secret.

    That escalation from a single relayed login to total control is a textbook case of privilege escalation: each step trades a small foothold for a larger one until the attacker holds the keys to the whole directory.

    Defending against NTLM relay

    Each defense closes a specific part of the gap. None of them alone is the whole answer, which is why they are usually layered.

    Signing, so the relay cannot stay silent in the middle

    Message signing binds the authenticated session to a key both legitimate parties share, so a man in the middle who merely forwards packets cannot tamper with or sustain the session. SMB signing, when required rather than merely offered, defeats SMB relay. The equivalent for the directory is LDAP signing, which protects relayed LDAP connections. Requiring signing turns a relayed session into a session the relay cannot actually use.

    Channel binding and Extended Protection for Authentication

    Signing still leaves protocols that ride inside TLS, like LDAPS and the AD CS web endpoint. The fix there is channel binding, delivered as Extended Protection for Authentication, EPA. Channel binding ties the NTLM authentication to the specific TLS channel it was sent over. When the attacker relays the authentication to a target over a different TLS channel, the binding no longer matches and the target rejects it. That is precisely the protection that closes ESC8: enabling and requiring EPA on the Certificate Authority web enrollment and certificate enrollment web services makes the relayed authentication fail the channel check. LDAP channel binding does the same for LDAPS.

    Disabling NTLM and mitigating coercion

    The most thorough fix is to stop using NTLM at all and rely on Kerberos, which does bind tickets to the target service. Disabling NTLM where it is no longer needed removes the relayable authentication entirely, though it takes auditing to find every dependency first. Alongside that, blunt the coercion triggers: apply the patch for CVE-2021-36942 to mitigate PetitPotam, disable the Print Spooler service on domain controllers where it is not needed to shut the PrinterBug, and filter the RPC traffic the coercion protocols ride on. Removing the trigger means the attacker cannot summon an authentication to relay even where NTLM still exists.

    It is worth placing this attack against its neighbors. NTLM relay is a failure of authentication binding, not of authorization. The victim’s identity is genuine and the target’s permission check is working correctly; the flaw is that the genuine identity arrived at a server it never meant to authenticate to, a distinction the boundary between authentication and authorization makes precise. The relay corrupts the who, and the rightful permissions of that who do the rest.

    The assumption that breaks

    Strip away the tools and the protocols and one assumption is left holding the whole thing up. NTLM assumes that proving you know a secret to one server means you meant to authenticate to that server. The handshake is careful about the secret and careless about the destination. It binds the proof to a random challenge and to nothing about where the proof is headed, so the proof is portable. A man in the middle does not need to break the cryptography, defeat the hash, or learn the password. They only need to move a valid answer from the server that was meant to receive it to a server that was not, and the second server, checking only that the answer is mathematically correct, lets the victim in.

    The bug is not a weak cipher or a careless administrator. It is a missing link between an authentication and its intended target, an assumption that the proof and the destination are the same thing when in fact one travels and the other does not. That kind of flaw does not show up by scanning for a known bad signature. It shows up by asking what each component assumes about identity and why it still trusts a credential that arrived from somewhere it did not expect. That is the kind of question an autonomous researcher built to test assumptions is meant to ask. Require signing, bind authentication to its channel, retire NTLM where you can, and shut the coercion triggers that feed the relay. Learn more about that approach on our about page.

    Frequently asked questions

    What is an NTLM relay attack in plain terms?

    It is a man in the middle attack on Windows authentication. NTLM is a challenge response protocol where a client proves it knows a password by signing the server’s random challenge, without the password ever crossing the wire. The catch is that the signed proof is not bound to the server it was meant for, so an attacker who receives one authentication can forward it verbatim to a different server and be accepted as the victim there. The MS-NLMP specification documents the three message handshake the relay abuses.

    How does an attacker get a machine to authenticate to them?

    Two ways. Passively, the tool Responder answers broadcast name resolution requests over LLMNR, NBT-NS, and mDNS, so a victim looking for a host connects to the attacker and authenticates. Actively, the attacker coerces a chosen machine. PetitPotam abuses the MS-EFSRPC protocol to force a target, even a domain controller, to authenticate over NTLM, and it is tracked as CVE-2021-36942. The PrinterBug and PrivExchange achieve the same coercion through other protocols.

    What can an attacker do once the authentication is relayed?

    It depends on the target. Relayed to LDAP on a domain controller, the attacker can configure resource based constrained delegation to later impersonate an admin. Relayed to SMB where the victim is a local admin, they can run code or dump secrets. The most severe target is the AD CS web enrollment endpoint: the ESC8 attack documented in SpecterOps’ Certified Pre-Owned paper relays a coerced domain controller authentication to AD CS, obtains a certificate for the DC, and escalates to full domain compromise. The relay itself is usually performed with the ntlmrelayx tool.

    How do you defend against NTLM relay?

    Layer the controls. Require SMB signing and LDAP signing so a man in the middle cannot use the forwarded session. Enable Extended Protection for Authentication, which binds the authentication to its TLS channel and is what closes the AD CS ESC8 path. Disable NTLM where it is no longer needed so there is no relayable authentication, and apply the patch for PetitPotam plus disable the Print Spooler on domain controllers to remove the coercion triggers. The ntlmrelayx tool ships in the Impacket toolkit, which is useful for testing whether these defenses actually hold.

  • What Is a Hash Flooding Attack and Why It Stalls a Server With Bytes

    What Is a Hash Flooding Attack and Why It Stalls a Server With Bytes

    A hash flooding attack is a low bandwidth denial of service that turns a data structure your server relies on against itself. A hash table promises constant time lookups, but that promise only holds when keys scatter evenly across buckets. If an attacker knows the hash function, they can craft hundreds of keys that all land in the same bucket, collapsing the table into a single long chain. Average case O(1) becomes worst case O(n) per operation, and building the table from n such keys costs O(n^2). A few hundred kilobytes of carefully chosen form fields or JSON keys, parsed automatically by the framework before any of your code runs, can pin a CPU core for seconds. This post walks the mechanism one step at a time: how a hash table actually stores keys, how an attacker engineers the collisions, why request parsing amplifies the damage, the 2011 wave that hit every major web platform at once, and the keyed hashing fix that closed the door.

    How a hash table earns its O(1) reputation

    A hash table is the workhorse behind every dictionary, map, and associative array you have ever used. The idea is simple. You have some keys, say the names of form fields, and you want to find the value for any key fast. Instead of scanning a list, the table keeps an array of buckets and runs each key through a hash function, a small piece of math that turns a string of bytes into a number. That number, taken modulo the number of buckets, tells the table which bucket the key belongs in.

    When two keys land in the same bucket, that is a collision, and it is normal. Real hash functions cannot map an unlimited set of strings to a fixed array without overlaps. The standard way to handle a collision is chaining: each bucket holds a small linked list, and colliding keys are appended to it. To look up a key, the table hashes it to find the bucket, then walks that bucket’s chain comparing keys until it finds a match.

    The reason this is fast is entirely about distribution. If a good hash function spreads n keys roughly evenly across n buckets, every chain is one or two entries long. Finding a key means hashing once and comparing once or twice, regardless of how many keys are in the table. That is the constant time, O(1), behavior everyone counts on. Inserting n keys one after another costs O(n) total, because each insert is O(1). The whole edifice of fast lookups rests on that even spread.

    The catch is that O(1) is an average, not a guarantee. It assumes the keys arriving at the table are not chosen by someone who wants them to collide. Drop that assumption and the same data structure behaves very differently.

    How a hash flooding attack engineers collisions on purpose

    Now suppose every key you insert hashes to the exact same bucket. The table never spreads anything. One chain grows longer with every insert while every other bucket sits empty. Looking up a key now means walking a chain of length n, comparing against every key already there. A single lookup is O(n) instead of O(1).

    Inserting is worse, because insertion has to check whether the key is already present before adding it. When you insert the kth colliding key, the table walks the existing chain of length k minus one to confirm the key is new. So the first insert does zero comparisons, the second does one, the third does two, and the nth does n minus one. The total work is 0 plus 1 plus 2 and so on up to n minus 1, which is n times n minus one over two. That is the quadratic blowup: building a table from n colliding keys costs on the order of n^2 comparisons rather than n.

    The math is what makes the attack so cheap for the attacker and so expensive for the server. Double the number of colliding keys and you quadruple the work. Ten thousand colliding keys is not ten thousand units of work, it is on the order of fifty million. A hundred thousand colliding keys is on the order of five billion comparisons, all to insert a payload that fits comfortably in a single request body. The attacker spends a few kilobytes of upload; the server spends seconds of one core grinding through a linked list.

    It helps to walk the asymmetry concretely. A parameter name like field0000 is about ten bytes on the wire. Ten thousand such names, separated by ampersands, is roughly a hundred kilobytes, a request body smaller than many images on a typical web page. An honest hundred kilobyte form with ten thousand distinct fields would insert into the table in about ten thousand operations, finishing in microseconds, because each key lands in its own bucket and the chains stay short. The same hundred kilobytes of colliding keys forces about fifty million comparisons, because every key has to be checked against the entire growing chain before it is added. The wire cost is identical. The CPU cost differs by a factor of five thousand. That ratio is the entire point of the attack: the work the server does is not proportional to the work the attacker does, and the gap between them widens with every key.

    The same quadratic curve also explains why the cap based mitigation discussed later actually works. The pain lives in the n^2 term, and n^2 is gentle for small n and brutal for large n. A thousand colliding keys is only about half a million comparisons, finished in a blink. Ten thousand is fifty million. A hundred thousand is five billion. Cutting the maximum n the parser will accept does not slow the attack by a constant factor, it moves you back down the steep part of the curve, where even adversarial input is cheap to process.

    Why knowing the hash function is the whole game

    None of this works if the attacker cannot predict where keys land. The collisions have to be engineered, and to engineer them you need to know the function. Many of the platforms hit in 2011 used a well known, fixed, non keyed hash. PHP arrays and a number of Java systems used Daniel Bernstein’s DJBX33A and DJBX33X functions, which are short, fast, and completely public. The function multiplies a running value by 33 and adds the next byte, so its behavior is easy to reason about and easy to reverse.

    With a fixed function and no secret, an attacker can compute, offline and ahead of time, large sets of distinct strings that all produce the same hash value. For DJBX33A there are well known short building blocks, pairs of two character strings that collide, and you can concatenate them to manufacture as many colliding keys as you like. The strings look like ordinary parameter names. There is nothing malformed about them. They simply happen to be chosen so the cheap public hash maps every one of them to the same number. The attacker does the hard combinatorial work once and reuses the result against every server running that function.

    The construction is worth understanding because it shows how little effort the attack takes once the function is known. Suppose you find two short strings, call them Aa and BB, that the hash maps to the same value. Because the hash processes a string one byte at a time, building the running value as it goes, any longer string built by gluing these blocks together in any order produces the same final value as long as the blocks are interchangeable at each position. Two interchangeable two byte blocks give you four colliding strings of four bytes, eight of six bytes, sixteen of eight bytes, and in general 2 to the power of the number of slots. A handful of base collisions, concatenated, yields an effectively unlimited supply of distinct keys that all hash to one bucket. The attacker never has to brute force the full set. They find a few small collisions and let concatenation multiply them. This is why the payload is cheap to generate and why every server running the same unseeded function is vulnerable to the same precomputed list.

    The request parsing amplifier

    An attacker still needs a way to get a server to insert thousands of attacker chosen keys into a hash table without writing any code on the server. Web frameworks hand them exactly that, for free, on every request.

    When a browser or a client sends a POST request with a form body or a JSON document, the framework parses it before your handler ever sees it. A body like a=1&b=2&c=3 is split on the ampersands and equals signs, and each name is inserted as a key into a dictionary so your code can read request.params["a"]. The same happens for JSON objects, for query string parameters, for multipart form fields, and in many stacks for HTTP headers and cookies. Parsing untrusted request data into a hash table is not an edge case. It is the single most common thing a web framework does, and it happens automatically, on the parsing path, with the keys taken verbatim from the request.

    That is the amplifier. The attacker does not need authentication, a vulnerable endpoint, or any application logic at all. They send one POST request whose body is nothing but colliding parameter names, a few hundred kilobytes of key1=&key2=&key3= where every key name is one of the precomputed collisions. The framework dutifully parses each one and inserts it into a single overloaded bucket, paying the quadratic cost on the way. One ordinary looking request, well under a megabyte, pins a core while the parser grinds. At demonstration bandwidth on the order of a slow home connection, a steady trickle of these requests was enough to keep a modern CPU core fully busy. The bandwidth to attack is trivial; the bandwidth to absorb the attack is the server’s entire core.

    The attacker never overwhelms the network or floods the server with volume. They send one small, well formed request and let the server’s own data structure do the expensive work, turning a few kilobytes of input into seconds of CPU.

    Consider our invented app, Acme Notes, which exposes a JSON API. A client posts a note as a JSON object, and the framework parses that object into a dictionary keyed by field name before validation. An attacker posts a single note whose JSON body has a hundred thousand keys, all engineered to collide. Acme Notes never gets to reject the note for being malformed, because the denial of service happens during parsing, inside the framework, before a line of Acme Notes code runs. The application looks blameless. The vulnerability lives one layer down, in the assumption that request keys are not adversarial.

    The 28C3 wave of 2011

    This stopped being theoretical at the end of 2011. At the 28th Chaos Communication Congress, Alexander Klink and Julian Walde presented Efficient Denial of Service Attacks on Web Application Platforms, and the impact was that it hit nearly every major web stack at the same time. PHP, Java based servers, Python, Ruby, and ASP.NET all parsed request parameters into hash tables built on predictable, non keyed hash functions. One technique, slightly retargeted per language, took them all down.

    The coordinated disclosure was tracked as oCERT-2011-003, which assigned a row of CVE identifiers across the affected platforms. PHP before 5.3.9 was CVE-2011-4885: it computed hash values for form parameters without restricting predictable collisions, letting a remote attacker burn CPU with many crafted parameters. Python was assigned CVE-2012-1150 under the same oCERT advisory. Ruby was CVE-2011-4815. On the Java side, Apache Tomcat was CVE-2011-4858, with sibling identifiers for Jetty, Glassfish, Geronimo, and the Rack middleware, among others. The point of the wave was not any single language. It was that an entire industry had independently reached for the same cheap public hash and the same automatic parameter parsing, and so shared the same flaw.

    It was not a new idea

    The class of attack was already eight years old in 2011. In 2003, Scott Crosby and Dan Wallach published Denial of Service via Algorithmic Complexity Attacks at the USENIX Security Symposium. They named the general category, algorithmic complexity attacks, where an attacker feeds an input crafted to drive a data structure or algorithm into its worst case rather than its average case. They demonstrated it against the hash tables in Perl and against the Bro intrusion detection system and the Squid proxy, knocking a Bro server over with less bandwidth than a dialup modem. They also pointed to the fix: universal hashing, where the hash function is parameterized by a secret the attacker does not know. The 2011 wave was the same attack the 2003 paper had described, finally cashed in against the web at scale.

    SipHash and the real fix

    The patches came in two flavors, and only one of them addresses the root cause.

    Capping the number of parameters

    The immediate, pragmatic mitigation was to limit how many parameters a request is allowed to carry. PHP’s fix for CVE-2011-4885 added a configuration directive, max_input_vars, defaulting to 1000, that caps the number of input variables parsed from a single request. If the quadratic cost only becomes painful past tens of thousands of keys, refusing to parse more than a thousand keeps any single request cheap. Other stacks added equivalent caps on parameter counts, header counts, and body sizes.

    This works, but it treats the symptom. The hash function is still predictable, so an attacker who finds any path that inserts more than the cap of attacker chosen keys, or any code that builds a large dictionary from untrusted input outside the parameter parser, can still trigger the blowup. A cap narrows the attack surface. It does not remove the property the attack depends on.

    Keyed hashing with SipHash

    The real fix is to make the hash function unpredictable, so the attacker can no longer compute colliding keys ahead of time. You introduce a secret key, chosen randomly at process startup, and mix it into the hash. The function still spreads keys evenly and runs fast, but its exact mapping is different in every process and unknown to anyone outside it. An attacker cannot precompute collisions for a function whose seed they cannot see. This is the universal hashing idea from the 2003 paper, made practical.

    The algorithm the ecosystem settled on is SipHash, a keyed hash function designed in 2012 by Jean Philippe Aumasson and Daniel Bernstein specifically in response to the hash flooding wave. SipHash is fast on the short strings that hash tables actually use as keys, and it takes a 128 bit secret key, so without that key you cannot find collisions. It was adopted as the keyed hash behind the default hash table implementations in Perl, Python, Ruby, and Rust, among others. Python exposed the seed through the PYTHONHASHSEED environment variable while it stabilized the change. Rust ships SipHash as the default hasher for its standard library hash map out of the box.

    The thing to hold onto is why a non keyed hash was the actual bug. A fixed public hash function is a contract the attacker can read. Once they know the function, the set of colliding keys is just a calculation, and the data structure has no defense because it cannot tell an adversarial key from an honest one. Adding a secret key changes the function from something public into something private, and the entire attack rests on the function being public. Cap the parameters if you like, but the function being predictable is the root, and a keyed hash is what pulls it out.

    If you want to see where this sits among related classes of bug, it shares DNA with the rest of the most common web vulnerabilities: a server trusting attacker controlled input to behave the way honest input does. Here the betrayed assumption is not about the content of a value but about the statistical shape of a set of keys. The closest relative is regular expression denial of service, where crafted input drives a backtracking regex into its worst case time instead of its average case, the same algorithmic complexity class seen from the regex engine; our free ReDoS regex analyzer checks a pattern for the runaway backtracking that makes that possible.

    The assumption that breaks

    Step back from the buckets and the chains and one assumption is holding the whole thing up. Every hash table assumes its keys are not chosen by an adversary who knows the hash function. That assumption is invisible in the textbook, where O(1) is stated as a fact rather than as an average over honest inputs. It is invisible in the framework, where parsing a request body into a dictionary looks like plumbing rather than a security boundary. And it was invisible across an entire industry that reached for the same fast public hash, until one conference talk made the cost visible everywhere at once.

    The bug was never a slow hash table or a careless parser. The bug was a data structure whose performance guarantee quietly depended on the goodwill of whoever supplied its keys, deployed on the one path where the keys come straight from an attacker. The fix was not to make the table faster. It was to remove the attacker’s ability to predict it, by making the function secret. That gap, between what a system assumes about its inputs and what an adversary can actually arrange, is the kind of flaw you find by asking what each component takes for granted and whether that thing can be chosen against it, rather than by scanning for a known bad string. It is exactly the kind of assumption an autonomous researcher built to test assumptions is meant to catch. Question the average case, key your hashes, and cap what you parse. Learn more about that approach on our about page.

    Frequently asked questions

    What is a hash flooding attack?

    It is a denial of service that abuses how hash tables store keys. A hash table gives O(1) average lookups only when keys spread across buckets. If an attacker knows the hash function, they can craft many keys that all collide into one bucket, so each operation degrades to O(n) and inserting n keys costs O(n^2). A small request of colliding keys then burns a CPU core. The class was first named by Scott Crosby and Dan Wallach in Denial of Service via Algorithmic Complexity Attacks at USENIX 2003.

    How does a small request cause so much load?

    Web frameworks parse request bodies, query strings, JSON keys, and headers into a hash table before your code runs. An attacker sends one POST request whose parameter names are all engineered to collide, often a few hundred kilobytes of key1=&key2= pairs. The framework inserts each name into a single overloaded bucket, paying the quadratic cost during parsing. The talk that demonstrated this across platforms was Efficient Denial of Service Attacks on Web Application Platforms at 28C3 in 2011.

    Which platforms were affected and what were the CVEs?

    The 2011 disclosure hit PHP, Java based servers, Python, Ruby, and ASP.NET at once, coordinated as oCERT-2011-003. PHP before 5.3.9 was CVE-2011-4885, Python was CVE-2012-1150, Ruby was CVE-2011-4815, and Apache Tomcat on the Java side was CVE-2011-4858, alongside identifiers for Jetty, Glassfish, Geronimo, and Rack. The shared cause was a predictable, non keyed hash function applied to attacker controlled request keys.

    How do you fix hash flooding?

    The real fix is keyed hashing: mix a secret seed chosen at startup into the hash so an attacker cannot precompute collisions. The ecosystem adopted SipHash, designed in 2012 by Jean Philippe Aumasson and Daniel Bernstein, as the default keyed hash in Perl, Python, Ruby, and Rust. Capping the number of parameters per request, such as PHP’s max_input_vars directive defaulting to 1000, helps as a mitigation, but a non keyed hash is the root problem because its collisions can be computed in advance.

  • How Rowhammer Works: Flipping Bits in Memory You Were Never Allowed to Touch

    How Rowhammer Works: Flipping Bits in Memory You Were Never Allowed to Touch

    Rowhammer is a hardware level attack that flips bits in memory the attacker was never allowed to touch. It works because DRAM stores each bit as a tiny charge in a cell, the cells are packed extremely close together, and repeatedly activating one row of cells leaks charge into the physically adjacent rows. Do it fast enough, often enough, and a bit in a neighboring row changes from a one to a zero or back, with no read or write permission on that row required. The bug lives below the software stack entirely, in the silicon, which is what makes it so unsettling. This post walks the mechanism one step at a time: how a DRAM cell holds a bit, what the activate and precharge cycle is, why the disturbance appears, how an attacker turns a random flip into a broken security boundary, and what the later browser and mobile variants and the ECC and TRR defenses actually do.

    What rowhammer is, in one paragraph

    A DRAM chip is a grid of cells, each a capacitor that holds a charge and a transistor that gates access to it. Charge present means one logical value, charge absent means the other. The cells are organized into rows, and reading or writing any cell means activating the whole row it sits in. The discovery behind rowhammer, published by Yoonsung Kim and colleagues at ISCA 2014, is that hammering one row over and over, activating it thousands of times in a short window, disturbs the charge in the rows next to it enough to corrupt their stored bits. The attacker reads and writes only rows they are allowed to touch. The damage lands in a row they are not. That gap between what the attack accesses and what it corrupts is the whole story, and it is why the original paper is titled Flipping Bits in Memory Without Accessing Them.

    How a DRAM cell stores a bit

    Start at the bottom. A single DRAM cell is one capacitor and one transistor. The capacitor either holds a charge or it does not, and that presence or absence is the bit. The transistor is a switch that connects the capacitor to a wire called a bitline when you want to read or write it. Because a capacitor leaks charge over time, DRAM is dynamic: every cell has to be refreshed periodically, read out and written back, or the bit decays into noise. On commodity hardware that refresh happens on a fixed interval, traditionally every 64 milliseconds for the whole array.

    Cells do not stand alone. They are wired into a grid of rows and columns. All the cells in one row share a wire called a wordline, and all the cells in one column share a bitline. To touch any cell, the chip raises the voltage on that cell’s wordline, which switches on every transistor along the row at once and connects all of those capacitors to their bitlines. You cannot read a single cell in isolation. You read its entire row into a buffer, then pick the column you wanted.

    The activate and precharge cycle

    Getting at a row is a two step dance. First the memory controller issues an activate command for the row. That raises the wordline, dumps the row’s charges onto the bitlines, and latches the result into a strip of sense amplifiers called the row buffer. Now the row is open and its columns can be read or written quickly. When you are done with that row and want a different one in the same bank, the controller issues a precharge, which closes the open row, writes its contents back into the cells, and resets the bitlines to a neutral level so the next activate can begin cleanly.

    Every activate sends a voltage swing down a wordline that runs right past its neighbors. A single activate is harmless. The trouble is what happens when you force the same row through the activate and precharge cycle again and again, as fast as the chip allows, thousands of times before the next scheduled refresh comes around to repair the neighbors. That is the literal hammering in rowhammer.

    Why shrinking process nodes made the disturbance appear

    This was not a problem on older, larger memory. As DRAM makers shrank the process node to pack more capacity into the same die, the cells moved physically closer together and each capacitor got smaller, holding less charge to begin with. Closer cells mean stronger electrical coupling between a wordline and its neighbors, and smaller charge means a flip needs less disturbance to push a cell across its threshold. Past a certain density the repeated voltage activity on one row started leaking enough into adjacent rows to corrupt them before the periodic refresh could top them back up. The ISCA 2014 study tested modules from the three major vendors and found a large majority of recent DRAM modules vulnerable to these disturbance errors. The defect is not a manufacturing mistake in one batch. It is a consequence of how dense modern DRAM has to be, and it gets harder to avoid, not easier, as each generation packs the cells tighter.

    From a hammer to a bit flip: single sided and double sided

    Knowing that hammering corrupts neighbors, the next question is how to hammer effectively. Two refinements matter, and both come down to a detail of the activate cycle: a row only disturbs its neighbors while it is being opened and closed, so you have to keep forcing fresh activates rather than reading the same already open row.

    To guarantee that, an attacker accesses two different rows in the same bank in a tight loop and flushes them from the CPU cache between accesses, so each loop iteration forces a real activate down to the chip instead of being served from cache or the row buffer. This is single sided hammering: pick a couple of aggressor rows, pound them, and hope the disturbance lands on whatever victim row happens to sit beside one of them. The cache flush is the subtle part. Modern CPUs cache memory aggressively, so a naive loop that reads the same address repeatedly never reaches the DRAM at all; the second read onward is served from cache and the chip is never activated. Early proofs of concept used an explicit cache flush instruction, on x86 the clflush instruction, to evict the line after each access and guarantee the next read goes all the way down to the memory chip. Two aggressor rows in the same bank also help here, because alternating between them forces the row buffer to close one and open the other every time, which is exactly the activate and precharge churn that produces disturbance.

    Double sided hammering is sharper. A row has two immediate neighbors, the one above and the one below. If the attacker can hammer both of a victim row’s neighbors, the rows at position N minus one and N plus one, the victim row in the middle absorbs disturbance from both sides at once. Project Zero found this technique flipped vastly more bits and was necessary to get results on many of the machines they tested. Double sided hammering needs the attacker to know which physical rows are adjacent, which takes some reverse engineering of how addresses map to rows, but the payoff is a much higher flip rate on a chosen target.

    Weaponizing a flip: the page table entry attack

    A random bit flip somewhere in physical memory is, on its own, just a crash or a glitch. Turning it into a security boundary break is the hard and clever part, and the canonical demonstration is the 2015 Google Project Zero post Exploiting the DRAM rowhammer bug to gain kernel privileges, by Mark Seaborn and Thomas Dullien. They built two working exploits on real Linux machines.

    The first targets page table entries. On a modern system the operating system keeps page tables that map a process’s virtual addresses to physical memory, and each page table entry, a PTE, names a physical page and the permissions on it. The attack works like this. The exploit first sprays memory so it is filled almost entirely with the process’s own page tables, then hammers until it finds a flip that lands inside a PTE. If the flipped bit changes the physical page number that the PTE points at, there is a good chance the PTE now points at a page that is itself one of the attacker’s page tables. The moment that happens, the process has a writable mapping of its own page table. It can edit page table entries directly, point them at any physical page it likes, and from there it has read and write access to all of physical memory, including the kernel. That is full privilege escalation driven by a single well placed flip in a structure the attacker was never allowed to modify.

    The attacker never writes to the page table. The hardware changes it for them, one bit at a time, from a row next door.

    The second Project Zero exploit escapes the Native Client sandbox, NaCl, which was a way to run untrusted native code safely in the browser by validating that the code only used a restricted set of instruction sequences. The attack hammers the sandboxed code itself. NaCl enforces safe indirect jumps by masking the target address with a fixed instruction sequence, and a bit flip that changes a register number inside one of those sequences can turn a safe, validated jump into an unsafe one that lands on an unaligned address. From that misaligned landing the attacker reaches instruction bytes the validator never checked, including hidden syscall instructions, and breaks out of the sandbox. Two different boundaries, the kernel and the sandbox, both broken by the same physical effect.

    Browser and mobile variants

    The early proofs of concept needed special conditions, a native binary and often a cache flush instruction. The research that followed steadily stripped those requirements away, which is the part of the story that turned rowhammer from a lab curiosity into a broad concern.

    Rowhammer.js: from the browser, no native code

    Rowhammer.js, by Daniel Gruss and colleagues, showed that the attack could be triggered from plain JavaScript running in a browser, with no native binary and no special CPU instruction to flush the cache. The researchers built a memory access pattern that evicts cache lines using ordinary accesses alone, so that the hammering reaches DRAM even without a flush instruction available to scripts. That made rowhammer a remote concern: a flip could in principle be induced by visiting a web page, narrowing the gap between the hardware defect and an ordinary attacker.

    Drammer: deterministic flips on Android and ARM

    Drammer, from the VUSec group, carried the attack to mobile. It demonstrated rowhammer on ARM based Android phones and, importantly, made the exploit deterministic rather than probabilistic. It did this by abusing the phone’s memory allocator to land a page table in a physical location the attacker had already found to be flippable, so the flip reliably hit a useful target. Drammer was a root privilege escalation that relied on no software vulnerability at all, only the hardware bug, on a class of devices many people assumed were out of reach.

    One location hammering

    Later work showed that on some systems you do not even need two aggressor rows. One location hammering repeatedly activates a single row, relying on the memory controller’s row policy to keep closing and reopening it so each access becomes a fresh activate. It works where the controller uses a closed page or adaptive policy, and it further trimmed the conditions an attacker needs to satisfy.

    Defeating the defenses: ECC and TRR

    Two mitigations were widely treated as the answer to rowhammer. Research has shown both can be defeated, which is the honest state of the field, even though both still raise the bar.

    Error correcting code memory and ECCploit

    ECC memory adds redundant bits so the controller can detect and correct errors, typically correcting a single bit flip in a word and detecting two. The intuition was that rowhammer flips would simply be corrected away. ECCploit, from VUSec, showed this is not a clean defense. By using timing side channels to learn how the ECC scheme behaves and carefully arranging multiple flips in the same word, an attacker can engineer corruption that slips past correction. ECC raises the cost and the number of flips required, but it does not make a vulnerable module safe.

    Target Row Refresh, TRRespass, and Half-Double

    Target Row Refresh, TRR, is a defense built into DDR4 memory. The idea is that the chip watches for rows being activated unusually often and proactively refreshes their neighbors before a flip can develop, repairing the victim before the disturbance accumulates. It was marketed as the fix that closed rowhammer for good. It did not. TRRespass, from VUSec, showed that TRR implementations track only a limited number of aggressor rows at once, so an attacker who hammers many rows at the same time, a many sided pattern, can overwhelm the tracker and still flip bits on DDR4 modules that TRR was supposed to protect. Half-Double, demonstrated by Google, exploits a different gap: as cells shrink further the disturbance reaches beyond the immediate neighbor to rows two steps away, and the very act of TRR refreshing a near neighbor can itself contribute disturbance to a row further out. Both results say the same thing. The in chip mitigations narrowed the attack but did not end it.

    What actually helps

    No single mitigation closes rowhammer cleanly, so defense is layered. Increasing the refresh rate, refreshing the whole array more often than the standard interval, gives disturbance less time to accumulate before a victim row is repaired, at a cost in performance and power. ECC and TRR each raise the number of flips or the precision an attacker needs, even though neither is sufficient alone. The deeper fixes are in hardware: probabilistic or counter based schemes that track how often each row is activated and refresh threatened neighbors accurately, and successor memory standards that build stronger row activation tracking into the specification rather than leaving it to a vendor’s opaque, limited TRR logic. The direction of travel is to move the defense into the silicon where the bug lives, because nothing in software can stop a charge from leaking between two cells the manufacturer placed a few nanometers apart. There are also operating system and allocator level mitigations that try to keep security sensitive structures like page tables physically away from memory an attacker can hammer, which raises the difficulty of the targeting step even when the underlying flip is still possible.

    It is worth being precise about what is demonstrated versus theoretical. The bit flips themselves, the PTE and NaCl exploits, the JavaScript and Android variants, and the bypasses of ECC and TRR are all demonstrated on real hardware in published research. What any given attacker can do against a specific deployed machine depends heavily on the exact memory modules, the controller policy, and the mitigations in place, and reliable exploitation in the wild is harder than a lab proof of concept. The bug is real and the exploits are real; the difficulty is in the targeting.

    Rowhammer also sits near other low level boundary breaks. Once an attacker flips a PTE and gains arbitrary physical memory access, what follows is privilege escalation in the classic sense, climbing from an unprivileged process to kernel level control. The difference is where the leverage comes from. Here it does not come from a logic bug in code. It comes from the memory itself betraying the software running on top of it.

    The assumption that breaks

    Step back from the wordlines and the page tables and one assumption is holding everything up. Every piece of software running on a computer trusts that memory it did not write cannot change underneath it. A program reads back what it stored. The kernel assumes its page tables say what it set them to say. The whole edifice of memory protection, of one process being walled off from another, rests on the substrate being inert, a passive box that holds bits faithfully until something with permission changes them. Rowhammer is the discovery that the substrate is not inert. Charge leaks between cells that were supposed to be independent, and an attacker with no permission on a row can reach into it through the silicon and change what it holds. The boundary everyone drew at the permission check actually ran somewhere lower, in the physics of how the bits are stored, and that lower boundary was never enforced at all.

    The bug is not a coding mistake you can find by reading the source. It is an assumption baked so deep into the model of computing that almost nobody thought to question it, that the hardware keeps your bits the way you left them. That kind of flaw, the unstated premise that the layer below you is trustworthy, is exactly what you find by asking what each layer trusts and why, rather than by scanning for a known bad pattern. It is the kind of assumption an autonomous researcher built to test assumptions is meant to catch, the ones nobody wrote down because they seemed too obvious to fail. Learn more about that approach on our about page.

    Frequently asked questions

    What is rowhammer and how does it flip bits?

    Rowhammer is a hardware level defect in DRAM. Each bit is a charge in a tiny capacitor, the cells are packed very close together, and repeatedly activating one row leaks charge into the physically adjacent rows until a bit in those neighbors flips. The attacker reads and writes only rows they are allowed to touch, but the corruption lands in a row they are not. The seminal study by Kim and colleagues, Flipping Bits in Memory Without Accessing Them, first characterized this disturbance error across DRAM from all three major vendors.

    What is the difference between single sided and double sided hammering?

    Single sided hammering pounds a small set of aggressor rows and hopes the disturbance lands on whatever victim row sits beside one of them. Double sided hammering targets both immediate neighbors of a chosen victim, the rows at N minus one and N plus one, so the victim in the middle absorbs disturbance from both sides at once. Google Project Zero reported in Exploiting the DRAM rowhammer bug to gain kernel privileges that double sided hammering flipped vastly more bits and was necessary on many machines they tested.

    How does a random bit flip become a kernel privilege escalation?

    An attacker sprays memory with their own page tables, then hammers until a flip lands inside a page table entry and changes the physical page it points at. With luck the entry now maps one of the attacker’s own page tables as writable, giving them direct edit access to address translation and from there read and write access to all of physical memory, including the kernel. The full Project Zero writeup walks both this PTE attack and a Native Client sandbox escape.

    Do ECC and Target Row Refresh stop rowhammer?

    They raise the bar but neither is a clean fix. ECCploit showed that carefully arranged multiple flips in one word can slip past error correction, and TRRespass showed that Target Row Refresh tracks only a limited number of aggressor rows, so a many sided hammering pattern can overwhelm it and still flip bits on DDR4. The VUSec TRRespass project page documents how built in TRR defenses were bypassed on real modules from all three major vendors.