Author: UnboundCompute

  • How UnboundCompute differs from a vulnerability scanner

    How UnboundCompute differs from a vulnerability scanner

    If you search for an ai vulnerability scanner, you will find a lot of tools that promise to find every bug in your app. Most of them work the same way underneath: they match your app against a list of known patterns and hand you back a long report of maybes. UnboundCompute is a different kind of tool, and this post is an honest look at how it differs and where it stands today.

    We are early. The product is being built. So this is not a sales pitch. It is a comparison of two ways of looking for bugs, and an explanation of why we chose the harder one.

    What a traditional vulnerability scanner actually does

    A classic scanner crawls your app, collects every URL, form, and parameter it can reach, then fires a fixed set of test payloads at each one. It watches the response for signs that something went wrong. A reflected string here, a database error message there, a slow response that hints at a sleep command.

    This works for whole classes of well known bugs. If a field echoes back <script>alert(1)</script> without encoding, a scanner will catch it. If a search box passes ' OR '1'='1 straight into a query, it will often catch that too. That is real value, and pattern matching is good at finding the obvious mistakes quickly.

    The trouble starts past the obvious. A scanner does not know what your app is for. It does not know that a user on a free plan should never reach /api/v1/exports/full, or that order id=1043 belongs to a different account. It sees a request that returns 200 OK and moves on. To the scanner, a working feature and a broken access control check look identical.

    Why the report is full of maybes

    Because a scanner guesses from surface signals, it has to play it safe. If a payload causes any change at all, it tends to flag it so it does not miss a real bug. The result is a report with many items marked “possible” or “medium confidence,” and a real chance that most of them are false positives. Someone on your team then spends a day or two checking each one by hand to find the few that are real.

    That is the core problem. The scanner did the easy part and left the hard part, proving the bug, to you.

    A scanner tells you where something might be wrong. The expensive work, proving whether it really is, still lands on a human.

    How an ai vulnerability scanner that reasons is different

    UnboundCompute is built around a different loop. Instead of matching payloads against a list, it tries to understand the app first, then form ideas about where the logic could break, then run experiments to test those ideas, and only report a finding once it has proof. Understand, assume, experiment, verify, chain.

    Here is what that looks like in practice on an invented example. Say a typical SaaS app called Acme Notes lets users share a note by id:

    GET /api/notes/4471
    Authorization: Bearer <user A token>

    A pattern matcher checks that the response is valid and moves on. A researcher that reasons about the app notices the id is a plain number and forms an assumption: the server might be trusting the id in the URL without checking who owns the note. So it designs an experiment. It logs in as a second user, takes that user’s token, and asks for a note id that belongs to user A:

    GET /api/notes/4471
    Authorization: Bearer <user B token>

    If user B gets back user A’s private note, that assumption was correct. The tool does not stop at a hunch. It confirms the note content belongs to a different account, records the exact request and response as evidence, and only then reports it. That is an access control bug a payload list would never spot, because nothing in the request looks malicious. The request is perfectly well formed. The problem is what the app assumed.

    Proof before report

    The rule that changes the output is simple: a finding is only reported when it is proven with concrete evidence. No proof, no report. This flips the work. Instead of handing you candidates to verify, the tool does the verification itself and hands you the ones that survived. The output is signal rather than a stack of maybes.

    A confirmed finding can also be turned into a repeatable check, so the same test keeps running and tells you if the bug ever comes back after a fix or a refactor.

    A short comparison

    • How it finds bugs. A scanner matches known patterns. UnboundCompute forms an idea about the app’s logic and tests it.
    • What it understands. A scanner sees URLs and parameters. The researcher tries to learn what the app is meant to do and where that intent could break.
    • What it reports. A scanner reports candidates, many of them false positives. UnboundCompute reports findings it has already proven.
    • Who proves the bug. With a scanner, a human triages the list. Here, the tool runs the experiment and keeps the evidence.
    • The kind of bug it catches. Scanners are strong on known payload bugs. The researcher reaches logic and access control flaws that have no fixed payload.
    • After the fix. A proven finding becomes a repeatable check that watches for the bug returning.

    We go deeper on this split in scanners vs research, since it is the line that matters most when you are choosing a tool.

    Where we are honest about the limits

    None of this means scanners are useless. They are fast, cheap, and good at sweeping for the common, known issues. If you have never run one, run one. The point is that pattern matching has a ceiling, and the highest impact bugs usually live above it, in the assumptions an app makes about who you are and what you are allowed to do.

    It also does not mean UnboundCompute is finished. It is not. We are building it, and we are not going to dress that up with customer counts or benchmark charts we do not have. What we can say is an early, encouraging 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. That is a hint that the approach works, not a promise of a result on your app.

    Which one should you use

    Think of them as different jobs. A vulnerability scanner is a smoke detector for the known stuff, cheap to run and worth keeping on. An autonomous researcher is closer to a person who reads your app, asks “what if the server trusts this id,” and goes and checks. They answer different questions.

    If you take one thing from this, take the difference between a maybe and a proof. A maybe costs you time. A proof saves it. That gap is exactly what an autonomous researcher that tests assumptions is built to close. You can read more about who we are and where we are headed on our about page.

    Frequently asked questions

    How is UnboundCompute different from a vulnerability scanner?

    A scanner crawls your app and fires a fixed set of known payloads at every input, then flags anything that looks suspicious. UnboundCompute instead tries to understand the app, forms ideas about where its logic could break, runs experiments, and reports a finding only once it has proof.

    Why do scanner reports contain so many false positives?

    A scanner guesses from surface signals, so it plays it safe and flags anything that changes, which produces a report full of items marked possible or medium confidence. Someone on your team then spends a day or two checking each one by hand, because the scanner did the easy part and left the proof to you.

    Should I stop using vulnerability scanners?

    No. Scanners are fast, cheap, and good at sweeping for common, known issues, and if you have never run one, you should. The point is that pattern matching has a ceiling, and the highest impact bugs usually live above it, in the assumptions an app makes about who you are and what you are allowed to do.

    Can I buy or try UnboundCompute today?

    Not yet. It is still being built, and we are not going to dress that up with customer counts or benchmark charts we do not have. You can read about who we are and where we are headed on our about page.


    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, a say in what it looks for, and founding pricing. If your team ships software worth pressure testing, apply to the design partner program.

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

  • How UnboundCompute works, from understanding an app to proving a bug

    How UnboundCompute works, from understanding an app to proving a bug

    This post is a deeper look at how UnboundCompute does ai penetration testing, walked through one step at a time with a concrete example. UnboundCompute is an autonomous security researcher for web apps and APIs. Instead of running a fixed list of payloads, it learns how an app is meant to work, forms an idea about where that logic could break, and proves a finding before it ever reports one.

    To make the steps real, we will use an invented app called Acme Notes. It is a simple note taking SaaS where users sign up, create notes, and share them with teammates. No real system is being attacked here. Acme Notes exists only so we can show the method on something you can picture.

    Why ai penetration testing starts with understanding, not payloads

    Most scanners begin from a catalog of known attacks and fire them at every input. That finds the bugs everyone already knows to look for. It misses the bugs that come from a specific app making a specific assumption.

    UnboundCompute starts somewhere else. Before it tests anything, it reads Acme Notes the way a careful new engineer would. It maps the routes, the request shapes, and the rules the app seems to enforce. For Acme Notes, that means noticing things like this:

    • A note is fetched with GET /api/notes/{id}.
    • Sharing a note is a POST /api/notes/{id}/share with a teammate email in the body.
    • The app appears to assume that only a note owner can share that note.

    That last line is the interesting one. It is not a payload. It is an assumption the app is making. The whole method points at assumptions like that, because the bugs with the most impact usually live there.

    The highest impact bugs come from understanding the app, not from matching patterns. So the first job is to learn the app, then ask where its own rules might not hold.

    Form an assumption about where it could break

    Once the app is understood, the next step is a clear guess. Not a vague worry. A testable claim about one rule that might not be enforced everywhere.

    For Acme Notes, here is the assumption to challenge:

    • The app checks ownership when you read a note, but it may not recheck ownership when you share one.

    This is a guess about how access control can quietly fail. The read path and the share path were probably written at different times by different people. It is common for one path to enforce a rule that the other forgot. The guess is specific, so we can design a test that either confirms it or kills it.

    Design an experiment

    A good experiment isolates one variable. We want to know whether a user who does not own a note can still act on it through the share endpoint.

    So we set up two accounts in the test app, Alice and Bob. Alice owns a note. Bob does not. Bob has a valid session because he is a normal signed in user. The experiment is simple. Bob asks the share endpoint to operate on Alice’s note id.

    The point is control. If Bob’s request needs Alice’s note id and Bob’s own token, and nothing else changes, then any result we see is caused by the one thing we are testing.

    Verify with hard evidence

    This is the step that separates a real finding from a maybe. We do not report a guess. We run the experiment and look at what the app actually does.

    Here is the kind of request the experiment sends, using Bob’s session against Alice’s note:

    POST /api/notes/9d2f/share HTTP/1.1
    Host: acmenotes.test
    Authorization: Bearer <bob_session_token>
    Content-Type: application/json
    
    { "email": "bob@evil.test", "role": "editor" }

    Note 9d2f belongs to Alice. The token belongs to Bob. If Acme Notes were enforcing ownership on this path, the right answer is 403 Forbidden and no change to the note.

    Proof is what the response shows. If the app instead returns this:

    HTTP/1.1 200 OK
    Content-Type: application/json
    
    { "note_id": "9d2f", "shared_with": "bob@evil.test", "role": "editor" }

    then the assumption was right and the bug is real. Bob, who never owned the note, just gave himself editor access to it. The evidence is concrete: a 200, the response naming Bob as an editor, and a follow up GET /api/notes/9d2f with Bob’s token now returning the note body. That follow up read is the part that turns a suspicious response into a proven one. We can see Bob holding access he should never have had.

    What counts as proof

    Proof is not a status code on its own. It is a short chain that any engineer can replay:

    • The exact request that should have been denied.
    • The response showing it was allowed.
    • A second request that confirms the new access is real, not just an echo.

    If any link is missing, the finding stays unproven and is not reported. No bug is reported until it is proven. That is the rule that keeps the output as signal instead of a stack of guesses someone else has to triage.

    Chain a confirmed finding into the next

    A proven finding is not the end. It is a new fact about the app, and facts open doors.

    Now that Bob can grant himself editor access to any note id, the next question writes itself. What can an editor reach that a stranger cannot? If editors can read attachments, and attachments are served from a shared store, then the access control gap on sharing may lead to reading files that belong to other teams. So the next experiment targets that, using the access Bob just proved he can get.

    This is the chaining step. Each confirmed finding becomes the starting point for the next assumption, so a single broken rule gets followed as far as it really goes, with evidence at every step.

    A finding can become a repeatable check

    Once the share endpoint bug is proven and fixed, the proof does not get thrown away. The exact request and the expected 403 become a check that runs again later. If a future change reintroduces the gap, the check catches it. A confirmed finding turns into a small guard that keeps watching for the bug coming back.

    Where this stands today

    We are early and honest about it. The product is being built. We are not claiming customers, benchmarks, or finished results.

    What we can say is encouraging. A frontier model drove this full method on its own and identified and verified real access control and injection issues in test applications it had not seen before. We treat that as an early signal that the approach works, not as a final score.

    The Acme Notes walkthrough is the whole idea in one example. Understand the app, assume where it could break, design a clean experiment, verify with evidence you can replay, then chain the result into the next finding. This is exactly the kind of logic bug an autonomous researcher that tests assumptions is built to find. If you want the fuller picture of who we are and where we are headed, read more on our about page.

    Frequently asked questions

    How does UnboundCompute actually find a bug?

    It works in a loop: understand, assume, experiment, verify, chain. It reads how the app is meant to behave, forms a testable guess about a rule that might not hold, designs an experiment that isolates one variable, and confirms the result with evidence before reporting anything.

    What counts as proof before something is reported?

    Proof is a short chain any engineer can replay: the exact request that should have been denied, the response showing it was allowed, and a second request that confirms the new access is real rather than just an echo. If any link is missing, the finding stays unproven and is not reported.

    What does chaining mean here?

    A proven finding is a new fact about the app, and facts open doors. Once one rule is shown to be broken, UnboundCompute uses that access as the starting point for the next assumption and experiment, so a single gap gets followed as far as it really goes, with evidence at every step.

    Is UnboundCompute finished and ready to use?

    No. We are early and the product is being built, and we are not claiming customers, benchmarks, or finished results. You can read more about where we are headed on our about page.


    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, a say in what it looks for, and founding pricing. If your team ships software worth pressure testing, apply to the design partner program.

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

  • Why we are building UnboundCompute

    Why we are building UnboundCompute

    We started UnboundCompute because of a gap we kept running into. Most automated security testing checks a fixed list of known bugs and stops there. That misses the flaws that hurt most, the ones that need a real understanding of how an app works, like broken access control and business logic abuse. This post explains the gap, why we think it matters, and what we are betting on.

    We are early. The product is being built. We would rather tell you what we believe and why than sell you on results we have not earned yet. So this is a point of view, written plainly.

    What most automated security testing actually checks

    A normal scanner works from a catalogue. It knows what a reflected script looks like, what a classic injection string returns, what an outdated library version means. It sends those patterns at every endpoint it can find and reports the matches. This is genuinely useful. It catches the well understood bugs fast, on a schedule no human could keep, and it never gets tired.

    But notice what that approach assumes. It assumes the dangerous bugs all look like something the tool has seen before. Many do not. Consider a request like this:

    GET /api/orders/8841
    Authorization: Bearer trial-user-token
    
    HTTP/1.1 200 OK
    { "id": 8841, "owner": "another-account", "total": 1290 }

    There is no malformed payload here. No quote to break a query, no script tag, no signature to match. Yet the trial user just read an order that belongs to someone else. That is broken access control, and a pattern matcher has nothing to match against, because the request looks perfectly ordinary. The bug lives in the rule the app forgot to enforce, not in the shape of the input.

    Why automated security testing misses the bugs that matter

    The highest impact flaws come from understanding what an app is trying to do, then asking what happens when you bend one of its rules. Two examples make the point.

    Broken access control

    An app decides who is allowed to see or change what. When a check is missing, one user can reach another user’s data by changing an id in a URL, or reach an admin route that was never linked from the menu. To find this, you have to know who the current user is supposed to be and what they should not be able to touch. A fixed payload list does not carry that idea.

    Business logic abuse

    Logic bugs are worse to automate, because the app is behaving exactly as written. The code is just wrong about its own rules. Picture a checkout that takes a discount code. A tool sending known strings will never think to apply the same code three times, or set the quantity to a negative number so the total drops below zero:

    POST /api/cart/apply
    { "code": "SAVE20", "quantity": -4 }

    Nothing about that request is malformed. It is a valid call that exploits a rule the app assumed no one would break. You only find it by understanding the flow first, then probing the assumption underneath it.

    The bugs that hurt most are not strange inputs to known holes. They are ordinary requests that break a rule the app forgot to enforce.

    Why skilled humans cannot cover the gap alone

    Human testers find these bugs. A good one reads the screen, guesses the business rules, and chases behavior no rulebook predicted. That is exactly the kind of judgment a payload catalogue lacks. The problem is supply.

    • They are scarce. The people who are genuinely good at this work are few, and demand far outruns them.
    • They are expensive. A deep manual test is a serious cost, so most teams can only afford it once or twice a year.
    • They cannot keep up with shipping. Teams deploy many times a week. A test run once a year cannot see the code that shipped last Tuesday.

    So you end up with two options that each fall short. Scanners run constantly but miss the bugs that need understanding. Humans understand but cannot run constantly. The deeper version of this comparison lives in our scanners vs research category, which goes through where each one earns its keep and where it does not.

    Our bet: an autonomous researcher that tests assumptions

    Here is what we are building toward. Instead of a tool that matches known payloads, an autonomous researcher that works the way a thoughtful human tester does. It learns how the application is meant to behave. It forms ideas about where that logic could break. It designs experiments to test those ideas. Then it proves a finding before it ever reports it. Understand, assume, experiment, verify, chain.

    The order of those words matters. Understanding comes first, because the bugs we care about only appear once you know what the app expects. The verify step matters just as much. A finding is only reported when it is backed by concrete evidence, so the output is signal, not a pile of maybes. Take the order example above. The researcher would not flag a “possible” issue. It would replay the request, show the other account’s data coming back, and hand you a result you can reproduce.

    That last step changes the cost of reading a report. Every false alarm costs someone an hour of triage, and after enough of them people stop reading. Proof cuts the noise. And once a finding is confirmed, it can become a repeatable check that keeps watching for the same bug coming back after a future deploy.

    Where we are, honestly

    We will not pretend we are finished. We are early, and the product is being built. We have no customers to name, no benchmark to wave around, and we are not going to invent one.

    What we can share is an early signal that keeps us going. 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 frame that as encouraging, not as proof. It is enough to tell us the bet is worth making, and not enough to claim the work is done.

    Why this is worth building

    The pattern is hard to ignore. Software ships faster every year. The bugs that cause the worst days, the leaked records and the abused workflows, are the ones that need understanding, not pattern matching. Scanners cannot supply that understanding, and skilled humans cannot supply enough of their time. Something has to test the assumptions an app makes, at the speed teams now ship, and prove what it finds before it interrupts anyone.

    That is the thing we are trying to build. An autonomous researcher that tests the assumptions your app makes and reports a bug only once it is proven. We are early and we know it, but this is the gap worth closing, and it is why UnboundCompute exists. If you want to follow along or tell us where we are wrong, read more on our about page.

    Frequently asked questions

    Why is UnboundCompute being built at all?

    Most automated security testing checks a fixed list of known bugs and stops, which misses the flaws that hurt most, like broken access control and business logic abuse. Those bugs need a real understanding of how an app works, and we are building an autonomous researcher to test the assumptions an app makes rather than match known payloads.

    Why can’t existing scanners or human testers cover this gap?

    Scanners run constantly but only catch bugs that look like patterns they already know. Skilled human testers understand an app and find logic bugs, but they are scarce and expensive, so most teams can only afford a deep manual test once or twice a year, which cannot keep up with code that ships every week.

    Do you have customers or proof it works?

    No. We are early and the product is being built, so we are not claiming customers, revenue, funding, or benchmarks. We share a point of view here rather than results we have not earned yet.

    What kind of bugs does this approach target?

    It targets bugs that come from understanding what an app is trying to do, then asking what happens when one of its rules is bent. Common examples are broken access control, where a user reaches data they should not, and business logic abuse, where a perfectly valid request exploits a rule the app assumed nobody would break.


    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, a say in what it looks for, and founding pricing. If your team ships software worth pressure testing, apply to the design partner program.

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

  • Meet UnboundCompute, an autonomous security researcher for web apps and APIs

    Meet UnboundCompute, an autonomous security researcher for web apps and APIs

    UnboundCompute is an autonomous security researcher for web apps and APIs. It reads an application the way a careful person would, builds a picture of how the app is meant to behave, then goes looking for the places where that intent quietly falls apart. This post explains who it is, what it does, and where it fits in the wider story of autonomous penetration testing.

    What UnboundCompute actually is

    Think of it as a researcher that never gets bored and never stops reading. You point it at a web app or an API. It studies the routes, the parameters, the responses, and the rules the app seems to enforce. From that it forms a working model of the system: who is supposed to do what, which actions need permission, and which inputs the app trusts.

    That model is the whole point. Most bugs that matter are not missing patches. They are gaps between what the app intends and what it allows. A user who can read another user’s invoice by changing one number in a URL. An endpoint that checks your login but forgets to check whether the record belongs to you. These are logic gaps, and you only see them once you understand the logic.

    The loop: understand, assume, experiment, verify, chain

    UnboundCompute works in a loop. Each step feeds the next, and the loop keeps tightening until there is either a proven finding or nothing left to test.

    Understand

    First it learns how the app is meant to work. It maps the surface and reads the behavior. If GET /api/orders/1042 returns your order, the researcher notes that orders are addressed by a simple number and asks the obvious follow up: what enforces that 1042 is yours?

    Assume

    Next it forms ideas about where the logic could break. This is the part a fixed checklist cannot do. The researcher reasons about the app in front of it, not a generic template. For an orders endpoint it might assume that ownership is checked at login but not at the record level. For a password reset flow it might assume the token is predictable or reusable.

    Experiment

    Then it designs a test for each idea and runs it. One assumption, one experiment. For the ownership idea, it requests a record it should not own:

    GET /api/orders/1043
    Authorization: Bearer <a different user's session>

    If that returns someone else’s order, the assumption held and there is a real access control bug to confirm.

    Verify

    This is the step that separates a researcher from a noise machine. A guess is not a finding. UnboundCompute only reports something when it can prove it with concrete evidence, the request that triggered the behavior and the response that shows the impact. The output is signal, not a pile of maybes you have to sort through by hand.

    A finding is only worth reporting when you can show the exact request that proves it. Everything else is a guess wearing a confident face.

    Chain

    Single bugs are useful. Chained bugs are how real damage happens. Once a finding is verified, the researcher asks what it opens up. A leaked email here, a guessable identifier there, an endpoint that trusts a value it should not. On their own each looks minor. Together they can add up to a full account takeover. Because UnboundCompute carries its model of the app through the whole loop, it can connect one verified result to the next instead of treating every test as a fresh start.

    Why this beats a scanner that checks a known list

    A traditional scanner is a list reader. It carries a set of known signatures and fires them at every input it finds. That has real value for catching the obvious and the already known. It also has a hard ceiling. A scanner that only checks a known list cannot find a bug that is not on the list, and the bugs that hurt most are almost never on any list.

    Here is the difference in one example. A scanner sends a SQL injection string at /search?q= and checks whether the response looks like a database error. Useful. But it will happily pass an endpoint like this:

    POST /api/account/transfer
    { "from": "acct_self", "to": "acct_other", "amount": 500 }

    There is no payload to match here. The bug, if there is one, is that the server never checks whether you own acct_self. No signature catches that. You catch it by understanding what the endpoint is for and testing the assumption it makes about who is calling it. We write more about this split between checking and researching in our scanners versus research category.

    • A scanner asks: does this input match a known bad pattern?
    • A researcher asks: what does this app assume, and what happens when that assumption is false?

    Both questions are fair. The second one is where the high impact findings live, and it is the question UnboundCompute is built around.

    Where this sits in autonomous penetration testing

    Autonomous penetration testing is the idea that a system can plan and run its own security tests, not just replay a script. UnboundCompute fits there, but with a specific stance: the value is not in running more checks faster. It is in reasoning about the target, testing assumptions, and proving impact before saying a word.

    Verification also pays off after the first run. Once a finding is confirmed, it can become a repeatable check that keeps watching for the same bug coming back. So the work is not throwaway. A proven issue today becomes a guard against regressions tomorrow.

    Where we are right now: honest version

    We are early. The product is being built, and we are not going to dress that up. We have no customer numbers to share, no benchmark to wave around, and we are not promising results we cannot back.

    What we will say is this. In our own 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. We read that as an early, encouraging signal that the approach holds, not as a benchmark and not as proof. There is a long way to go from a good signal to a tool you can rely on every day, and that gap is the work in front of us.

    The short version

    UnboundCompute is a security researcher that runs on its own. It learns how an app is meant to work, guesses where the logic breaks, tests those guesses, and only reports what it can prove. That is a different job from a scanner reading a list of known payloads, and it is the job we think matters most. If you want to know who is building this and why, read more on our about page.

    Frequently asked questions

    What is UnboundCompute?

    UnboundCompute is an autonomous security researcher for web apps and APIs. It learns how an application is meant to behave, forms ideas about where that logic could break, runs experiments to test those ideas, and only reports a finding once it is proven with concrete evidence.

    Is UnboundCompute available to use yet?

    Not yet. We are early and the product is still being built, so we have no customers, revenue, or benchmarks to share and we will not invent any. If you want to follow the work, read more on our about page.

    How is this different from a vulnerability scanner?

    A scanner reads a fixed list of known payloads and fires them at every input, which catches obvious and already known issues. UnboundCompute instead reasons about what the app assumes and tests those assumptions, so it can reach access control and logic gaps that no fixed signature would match.

    Why does it report only proven findings?

    A guess is not a finding. UnboundCompute reports something only when it can show the exact request that triggered the behavior and the response that proves the impact, so the output is signal rather than a pile of maybes you have to triage by hand.


    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, a say in what it looks for, and founding pricing. If your team ships software worth pressure testing, apply to the design partner program.

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

  • Teardown: chaining small bugs into a real breach

    Teardown: chaining small bugs into a real breach

    Most reports score a bug on its own, then move on. That habit hides the real danger, because exploit chaining is how three small issues that each look harmless turn into one account takeover. In this teardown we walk through an invented app called Acme Notes and follow a chain from a leaky endpoint to a full password reset, link by link, proving each step before we connect it to the next.

    What exploit chaining means

    A chain is a sequence of findings where the output of one becomes the input of the next. Alone, each link earns a low severity rating. Read in order, they hand an attacker something they should never reach. The exploit chain meaning is simple to state and easy to miss: severity is not a property of one bug, it is a property of the path.

    Acme Notes is a small notes app. Users sign up, write notes, and reset a forgotten password by email. We found three issues. A public endpoint that lists user ids. An access control gap that returns a reset token for any id you ask for. A reset flow that accepts that token without a second check. Each was filed by a different reviewer as low. Together they are critical.

    Severity is not a property of one bug. It is a property of the path an attacker can walk end to end.

    Link one: a public endpoint leaks user ids

    Acme Notes has a directory feature so teammates can find each other. The endpoint needs no auth and returns a tidy list.

    GET /api/v1/directory?team=acme HTTP/1.1
    Host: app.acmenotes.example
    
    200 OK
    [
      { "id": 4821, "name": "Dana Lee" },
      { "id": 4822, "name": "Sam Ortiz" }
    ]

    On its own this reads as minor. Names are semi public anyway, and the team field is guessable. The reviewer who filed it wrote “info disclosure, low” and they were right about the impact in isolation. What matters for a chain is not the names. It is the id field. We now have a clean list of valid internal user ids, the exact input the next link wants.

    Why prove it first

    Before treating this as link one, we confirmed the endpoint really needs no session. We sent the request with no cookie and with a logged out client. Same 200, same ids. That is the evidence. We do not assume the ids are real or stable, we test that the same id maps to the same user across requests. It does. Now the link is verified and we can build on it.

    Link two: an IDOR exposes a reset token tied to an id

    Acme Notes lets a signed in user view their own pending reset status, so the support team can tell people whether a reset email is still valid. The route takes a user id.

    GET /api/v1/users/4821/reset_status HTTP/1.1
    Host: app.acmenotes.example
    Authorization: Bearer <any valid user token>
    
    200 OK
    { "pending": true, "token": "f3a9c1e8b2d47..." }

    This is an insecure direct object reference. The server checks that you are logged in. It never checks that the id you asked for is your own. So any authenticated user, even a brand new free account, can read the reset status of any other id, and the response includes the live reset token.

    Filed alone, this looks like a leak of a value that should be secret but that an attacker cannot target, because how would they know which ids exist or matter? That assumption is the weak point. Link one already answered it. We have the id list, so we are not guessing.

    Verify, then connect

    We confirmed the IDOR with two accounts we controlled. From account A we requested the reset status of account B by its id and read back B’s token. We did not stop at “the field is present.” We checked that the token value actually belonged to B’s account and not a placeholder. Only after that evidence did we treat link one and link two as joined.

    Link three: a weak reset flow accepts the token

    The final link is the reset endpoint itself. A well built flow ties the token to a session, an email confirmation, or a short expiry plus a one time use guard. Acme Notes does none of that. It accepts any token that matches a pending reset and sets the new password.

    POST /api/v1/password/reset HTTP/1.1
    Host: app.acmenotes.example
    Content-Type: application/json
    
    { "token": "f3a9c1e8b2d47...", "new_password": "attacker_chosen" }
    
    200 OK
    { "status": "password_updated" }

    On its own the team rated this medium and noted the token “is hard to obtain.” True in a vacuum. Links one and two removed that condition. The token is no longer hard to obtain, it is a field in a JSON response any user can read.

    Reading the chain end to end

    Put the three verified links in order and the picture changes:

    • Step one. Pull the user id for a target from the public directory.
    • Step two. Use any logged in account to read that id’s reset status and copy the live token.
    • Step three. Submit the token to the reset endpoint and set a new password.

    The result is account takeover of any user, starting from a free signup. None of the three findings would have triggered a page on their own. The chain is the bug. This is the gap between scanning for known payloads and understanding what an app assumes about its own data, a theme we cover across our attack teardowns.

    The defensive lesson

    The fix is not only to patch each link, though you should. It is to stop trusting that a low severity finding stays low. Three habits help.

    • Treat identifiers as reachable. Once an id appears in any unauthenticated response, plan as if every attacker holds the full list. Sequential integer ids make this worse, so prefer unguessable values, but do not rely on secrecy of ids as a control.
    • Check ownership on every object route. The IDOR existed because the server confirmed authentication but never authorization. “Is this caller allowed to see this specific record” is a separate question from “is this caller logged in.” Ask both.
    • Bind reset tokens to context. A reset token should be single use, short lived, and tied to the email that requested it or the session that follows the link. A token that any holder can redeem is a password waiting to be changed.

    The wider lesson is about how you review. When you file a finding, write down what the next attacker would need to make it worse, and whether your own app already provides that. The reset bug looked safe only because the reviewer assumed tokens were hard to reach. A second reviewer looking one step ahead would have asked where reset tokens are exposed, and found link two.

    How to verify a chain honestly

    Do not claim a chain you have not walked. Reproduce each link with evidence: the raw request, the raw response, and the accounts you used. When you write up indicators like the endpoints, hosts, and tokens involved, our free IOC extractor and defanger pulls those indicators out of your notes and defangs any live URLs so a report can be shared without anyone clicking something by accident. Confirm that the value carried between links is the real value, not a lookalike. Then walk the whole path once, from public directory to changed password, on accounts you own in a test environment. If any link fails to reproduce, the chain is a theory, not a finding.

    Closing

    Small bugs are not small when they line up. The way to catch a chain is to understand the app, question each assumption, and prove every link before you trust it. This is exactly the kind of problem an autonomous researcher that tests assumptions, rather than matching a fixed list of payloads, is built to find. You can read more about that approach on our about page.

    Frequently asked questions

    What is exploit chaining?

    A chain is a sequence of findings where the output of one becomes the input of the next, so three issues that each look harmless on their own can combine into something critical like an account takeover. The key idea is that severity is not a property of one bug, it is a property of the path an attacker can walk end to end. The teardown shows this on an invented app called Acme Notes for teaching, not as a real engagement.

    Why do reviewers underrate bugs that later form a chain?

    Each link is filed in isolation, often by a different reviewer, and rated low because a precondition looks hard to meet. A reset token leak gets called minor because the token seems hard to obtain, but an earlier link that exposes the id list removes exactly that condition. A reviewer looking one step ahead would ask what the next attacker needs and whether the app already provides it.

    How do you defend against chained exploits?

    Treat identifiers as reachable, so once an id appears in any unauthenticated response you plan as if every attacker holds the full list. Check ownership on every object route, since being logged in is a separate question from being allowed to see a specific record. Bind reset tokens to context so they are single use, short lived, and tied to the email or session, and review the broader Broken Access Control guidance.

    How do you verify a chain honestly?

    Do not claim a chain you have not walked. Reproduce each link with the raw request, the raw response, and the accounts you used, confirm the value carried between links is the real value and not a lookalike, then walk the whole path once on accounts you own in a test environment. If any link fails to reproduce, the chain is a theory, not a finding.


    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, a say in what it looks for, and founding pricing. If your team ships software worth pressure testing, apply to the design partner program.

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

  • Teardown: how an IDOR quietly exposes another user’s data

    Teardown: how an IDOR quietly exposes another user’s data

    This is an idor example built from scratch so you can watch how one quietly exposes another user’s data. We will use an invented app called Acme Notes, map how it works, form an assumption about a weak spot, then test it with real requests. Nothing here touches a live system. The goal is to teach how the bug works and how to spot it before an attacker does.

    What an idor example actually is

    IDOR stands for insecure direct object reference. It happens when an app uses an id from the request to look up a record, but never checks that the person asking is allowed to see that record. The id is the direct object reference. When ownership is not verified, the reference becomes insecure. That gap is the whole bug.

    This bug is common for one reason. Developers think about authentication, who you are, far more than authorization, what you are allowed to touch. Acme Notes asks you to log in. It forgets to ask whether the note you requested is yours.

    An IDOR is rarely about a clever payload. It is the server trusting a number it should have checked.

    Step one: map the app like a researcher

    Before testing anything, understand how the app is meant to work. Acme Notes is a small notes tool. You sign in, you see a list of your notes, you click one to read it. Open the browser network tab and watch what the page sends. When you click a note, the front end makes this request:

    GET /api/notes/4012 HTTP/1.1
    Host: app.acmenotes.example
    Authorization: Bearer eyJhbGciOiJI (your token)
    Accept: application/json

    The server answers with the note as JSON:

    HTTP/1.1 200 OK
    Content-Type: application/json
    
    {
      "id": 4012,
      "owner_id": 88,
      "title": "Q3 launch checklist",
      "body": "Ship the billing page before Friday."
    }

    Two facts stand out. The note id, 4012, is a plain sequential number that sits right in the URL. The response also carries an owner_id. Your account is owner 88. So the app knows who owns the note. The question is whether it checks that ownership on every read.

    Step two: form the assumption

    Good testing starts with a guess you can prove or disprove. Here the assumption is direct: the server may load a note by id without confirming the requester owns it. Sequential ids make this worth testing, because note 4011 and note 4013 almost certainly belong to other users. If the server only checks your token and then trusts the id, you can read notes that are not yours.

    An attacker would form the same assumption. The difference is that a researcher tests it on an app they control or have permission to test, and reports it so it gets fixed.

    Step three: test by requesting a neighbouring id

    Keep your own valid login. Change only the id in the URL. Ask for the note next door:

    GET /api/notes/4011 HTTP/1.1
    Host: app.acmenotes.example
    Authorization: Bearer eyJhbGciOiJI (your token)
    Accept: application/json

    If the app is safe, you should get a refusal. Something like this:

    HTTP/1.1 403 Forbidden
    Content-Type: application/json
    
    { "error": "You do not have access to this note." }

    But Acme Notes is not safe. It returns the note in full:

    HTTP/1.1 200 OK
    Content-Type: application/json
    
    {
      "id": 4011,
      "owner_id": 73,
      "title": "Investor call notes",
      "body": "Runway is tight. Do not share outside the board."
    }

    Look at owner_id. It is 73, not 88. You are logged in as 88, yet the server handed you another user’s note. That is the bug, proven in one request.

    Step four: confirm it is real, not a guess

    One odd response is not proof. Before you call this a finding, rule out the boring explanations. A careful check answers a few questions.

    • Is the data really someone else’s? The owner_id in the response differs from your account id. Log in as a second test user, note their real id, and confirm the leaked note belongs to a third party, not to you under another label.
    • Does it repeat? Request 4010, 4009, 4008. If a range of ids you do not own all return 200 with full bodies, this is a pattern, not a fluke.
    • Is the token doing anything? Send the same request with no Authorization header. If that returns 401 but a valid token for the wrong user returns 200, the app checks login but not ownership. That is the exact shape of an IDOR.
    • Can you see the write side too? Try a read only method first. Only test edits or deletes on data you are allowed to change, so you never damage real records while confirming the issue.

    When the leaked owner id is consistently not yours, the behaviour repeats across a range, and a valid login is the only thing the server checks, you have evidence rather than a hunch. That is the line between a real finding and noise. For more on how access control bugs are grouped and tested, see access control.

    Step five: assess the impact

    Impact is about what an attacker can reach and how easily. In Acme Notes, ids are sequential and the endpoint returns full note bodies. A script can count from 1 upward and pull every note in the system in minutes. That turns one weak check into a full data exposure.

    Now widen the lens. The same pattern often appears on more than one route. If /api/notes/{id} is broken, test the siblings the same way:

    • /api/invoices/{id} for billing records
    • /api/users/{id}/profile for personal details
    • /api/files/{id}/download for attachments

    One missing ownership check is bad. The same check missing across several endpoints is how a small bug becomes a breach. This is why one confirmed finding is worth turning into a repeatable test, so the same gap cannot return on a new route later.

    How to fix an insecure direct object reference example

    The fix is not to hide the id or scramble it. Hiding the reference only slows an attacker down. The real fix is to check ownership on the server, on every request, every time.

    Check ownership at the data layer

    Bind the lookup to the logged in user. Instead of fetching a note by id alone, fetch it by id and owner together:

    -- weak: trusts the id from the request
    SELECT * FROM notes WHERE id = 4011;
    
    -- safe: ties the note to the caller
    SELECT * FROM notes
    WHERE id = 4011 AND owner_id = :current_user_id;

    If the second query returns no rows, the app returns a 404 or 403. The user never learns whether the note exists, so they cannot map your id space by probing.

    Centralise the rule and add a regression test

    Put the ownership check in one place that every route calls, not copied into each handler where one can be forgotten. Then write a test that logs in as user A, requests user B’s note, and fails the build if the response is anything but a refusal. That test is what keeps the bug from coming back during the next refactor.

    What to take away

    An IDOR is a trust mistake, not a complex exploit. The app trusts an id it should have checked against the logged in user. You find it by mapping the app, noticing a guessable reference like a sequential note id, assuming ownership might not be verified, and proving it with a single request that returns someone else’s data. You fix it by checking ownership on the server for every object, every time.

    This is exactly the kind of bug an autonomous researcher that tests an app’s assumptions is built to find, because it comes from understanding how the app should behave, not from matching a known payload. If that approach is useful to you, read more about UnboundCompute.

    Frequently asked questions

    What is an IDOR vulnerability?

    IDOR stands for insecure direct object reference. It happens when an app uses an id from the request to look up a record but never checks that the person asking is allowed to see that record, so the reference becomes insecure. It is common because developers think about authentication, who you are, far more than authorization, what you are allowed to touch. See PortSwigger on IDOR.

    How is an IDOR found in practice?

    You map the app and watch the requests, notice a guessable reference such as a sequential note id in a URL, and form the assumption that the server may load the record without confirming ownership. Then you keep your own valid login, change only the id to a neighbouring value, and see whether the server hands back a record that is not yours. The teardown uses an invented app called Acme Notes purely to illustrate this, not a real engagement.

    How do you confirm an IDOR is real and not a fluke?

    Rule out the boring explanations first. Check that the leaked owner_id really differs from your account, that a range of ids you do not own all return full bodies so it is a pattern, and that sending no Authorization header returns 401 while a wrong user’s valid token returns 200, which shows the app checks login but not ownership. Test read only methods so you never damage real records.

    How do you fix an IDOR?

    Do not just hide or scramble the id, since that only slows an attacker down. Bind the lookup to the logged in user, for example fetch a record by id and owner together so a non owner gets a 404 or 403, centralize that ownership check in one place every route calls, and add a regression test that requests another user’s record and fails the build on anything but a refusal. This class of weakness maps to CWE-639.


    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, a say in what it looks for, and founding pricing. If your team ships software worth pressure testing, apply to the design partner program.

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

  • How do hackers find vulnerabilities?

    How do hackers find vulnerabilities?

    Ask most people how do hackers find vulnerabilities and they picture a tool that scans an app and spits out a list of holes. That happens, but it is the weak version. The strongest finding comes from a person sitting with an app, working out how it is meant to behave, then probing the spot where that intent quietly breaks.

    How do hackers find vulnerabilities by reasoning, not just scanning

    A scanner fires a fixed set of payloads at every field it can see and waits for a known pattern in the response. It is fast and it catches old, well documented bugs. It is also blind to the logic of the app. It does not know that an account ID in a URL was never supposed to be editable, or that a coupon code should only apply once. A researcher does know, because the researcher first learns the rules.

    So the real process is closer to detective work than to button pushing. You map the app. You learn what it promises. You guess where those promises are enforced by hope instead of by code. Then you test that exact guess.

    The best bugs are not hidden. They sit in plain sight, in the gap between what the app assumes and what it actually checks.

    Step one: map the application

    Before any testing, you build a picture of the app. What pages exist, what actions they offer, what data they touch. You watch the network traffic while you click around as a normal user. Every request and response is a clue about how the backend is wired.

    Take an invented example, a notes app called Acme Notes. As you use it, you notice a request like this when you open one of your own notes:

    GET /api/notes/4812 HTTP/1.1
    Host: app.acmenotes.example
    Authorization: Bearer your_token_here

    That single line tells you a lot. Notes are addressed by a plain number. Your note is 4812. The obvious question follows on its own. What happens if you ask for note 4811?

    What you are looking for while mapping

    • Identifiers you can change. Numbers and slugs in URLs and request bodies, like user_id, order=1099, or file=report.pdf.
    • Hidden actions. Buttons that only admins see, but that may still call an endpoint anyone can reach.
    • State the app tracks. Cart totals, account balances, draft versus published flags, anything the app expects to control.
    • Trust boundaries. The line between what the browser sends and what the server is willing to believe.

    Step two: understand how it is meant to work

    This is the part scanners skip. You read the app the way its designers read it. A note belongs to one user. A user should see only their own notes. An order total should equal the sum of its items. A password reset link should work once and then die.

    Each of those sentences is a rule. Each rule is a promise the app makes. The interesting question is always the same. Is this promise enforced on the server, or only suggested by the screen?

    Step three: form ideas about where assumptions break

    Now you turn rules into guesses. A good guess is specific and testable. Vague suspicion gets you nowhere. Concrete bets get you findings.

    • The server checks that you are logged in, but maybe it never checks that note 4811 is yours.
    • The price comes from a hidden form field, so maybe the server trusts whatever price the browser sends.
    • The reset token is a short number, so maybe you can guess another user’s token.
    • The admin panel link is hidden in the menu, but maybe POST /api/admin/users answers anyone who calls it.

    Notice the shape of every guess. The app assumes something. You bet that the assumption is checked in the wrong place, or not at all.

    Step four: test inputs and access

    With a guess in hand, you design the smallest experiment that would prove it. For the Acme Notes guess, you keep your own valid login but change one number:

    GET /api/notes/4811 HTTP/1.1
    Host: app.acmenotes.example
    Authorization: Bearer your_token_here

    If the response is 403 Forbidden or 404 Not Found, the promise held. The app checked ownership. You move on. If the response is 200 OK and you are reading a stranger’s private note, you have found a broken access control bug, the kind often called an insecure direct object reference.

    The same habit applies to input. If a search box builds a database query, you send a value that would break out of the intended query and watch how the app reacts. If a file name is echoed into a page, you send a value that would run as script and see whether the app cleans it. You are always asking one thing. Does the server defend this, or did it assume nobody would try?

    Step five: confirm impact

    A surprising response is not yet a finding. A guess is not evidence. You confirm. You read another account’s data on purpose, then read a second one to show it was not a fluke. You change a price to 0 and complete a checkout to show money actually moved. You prove the bug does what you claim, with a clear request and response that anyone can repeat.

    This is where honest work separates itself from noise. A confirmed bug with a reproduction is something a team can fix today. A list of maybes from a scanner is something a team has to triage, often only to find that most entries are false alarms.

    Blind scanning versus reasoning about the app

    Both approaches exist, and they fail in different ways. The difference is worth keeping straight, which is why we wrote a whole piece on scanners versus research.

    • Blind scanning throws known payloads at everything and matches known patterns. It finds the bug everyone already knows about. It misses logic flaws because it never learns the logic.
    • Reasoning about the app learns the rules first, then targets the exact place a rule is likely unenforced. It finds the access control and business logic bugs that scanners walk straight past.

    You can sum up the whole method in five words. Understand, assume, experiment, verify, chain. Learn the app. Bet on a broken assumption. Run a small test. Prove the impact. Then see whether one bug opens the door to the next.

    Why this matters for defenders

    If you build software, the lesson points straight at your code. Attackers will model your app’s rules and then check, one by one, whether each rule is enforced on the server. So enforce them on the server. Check ownership on every object lookup, not just login. Recompute prices and totals from trusted data, never from the request. Treat every value from a browser as a claim to verify, not a fact to trust. It also helps to make yourself easy to reach when a researcher does find something: publishing a security.txt file gives them a clear contact for responsible disclosure, and our free security.txt generator and validator builds and checks one for you.

    Finding vulnerabilities, done well, is just disciplined curiosity about where an app’s assumptions and its checks part ways. This is exactly the kind of bug an autonomous researcher that tests assumptions is built to find, working through understand, assume, experiment, and verify on its own. You can read more about that approach on our about page.

    Frequently asked questions

    How do hackers actually find vulnerabilities?

    The strongest approach is closer to detective work than to running a tool. You map the app, learn the rules it promises to enforce, guess where a rule is checked by hope instead of by code, then run the smallest test that would prove your guess. A scanner is faster but blind to logic, so it finds known bugs and walks past access control and business logic flaws.

    What is the difference between scanning and reasoning about an app?

    Blind scanning throws known payloads at every field and matches known response patterns, which finds the bug everyone already knows about but misses anything that depends on the app’s logic. Reasoning about the app learns the rules first, then targets the exact place a rule is likely unenforced, which is how access control and logic bugs surface. The OWASP Web Security Testing Guide describes structured manual testing.

    What do researchers look for when mapping an application?

    Identifiers you can change like a numeric user_id or order=1099, hidden actions such as an admin only button that still calls a reachable endpoint, state the app tracks like cart totals and balances, and the trust boundary between what the browser sends and what the server is willing to believe. Each is a clue about how the backend is wired.

    Why is confirming impact a separate step?

    A surprising response is not yet a finding, because a guess is not evidence. You confirm by reproducing the issue on purpose, for example reading a second account’s data to show it was not a fluke, and presenting a clear request and response anyone can repeat. A confirmed bug is something a team can fix today, while a list of maybes only creates triage work.


    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, a say in what it looks for, and founding pricing. If your team ships software worth pressure testing, apply to the design partner program.

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

  • What is command injection? Examples explained

    What is command injection? Examples explained

    Command injection is one of the oldest and most dangerous web bugs, and it is also one of the easiest to understand once you see it in action. It happens when an app takes input from a user, drops that input into a system command, and runs the whole thing in a shell. If the app trusts the input too much, the user can append their own commands and make the server run them.

    What command injection means

    The short version of the command injection meaning is this: your app wanted to run one command, but the attacker tricked it into running two. The first is the command you intended. The second is whatever the attacker tacked on. The shell happily runs both because, to the shell, it is just text.

    The root cause is mixing two things that should stay apart: data (the value a user typed) and code (the command the server runs). When user data flows straight into a command string, the data can change what command runs. That is the whole bug in one sentence.

    The app meant to run one command. The attacker made it run two. The shell cannot tell your intent from their input, so it runs both.

    A simple command injection example

    Let us invent a small app called Acme Netcheck. It is a network tool with one feature: you give it a hostname, and it pings that host so you can see if the host is reachable. The form has one field named host, and the backend runs a ping for you.

    Here is the kind of code that causes the problem. This is written to show the mistake, not to copy:

    # DANGEROUS: user input goes straight into a shell command
    host = request.form["host"]
    command = "ping -c 1 " + host
    output = os.popen(command).read()
    return output
    

    If a normal user types example.com, the server builds and runs this:

    ping -c 1 example.com
    

    That works as intended. The trouble starts when someone types something that is not just a hostname. On a typical shell, a semicolon ends one command and starts another. So an attacker types this into the same field:

    example.com; whoami
    

    Now the server builds and runs this:

    ping -c 1 example.com; whoami
    

    The shell runs the ping, then runs whoami, and the app returns the output of both. The attacker just learned which user the web server runs as. They did not break into anything clever. They only added a semicolon and a second command to a field that was supposed to hold a hostname.

    Other command injection examples that work the same way

    The semicolon is one of several shell characters that chain or redirect commands. These all let an attacker smuggle a second command into a single input field:

    • example.com && whoami runs whoami only if the ping succeeds.
    • example.com | whoami pipes the first command into the second.
    • $(whoami) or `whoami` runs the inner command and pastes its result back in.

    These are command injection examples you will see again and again because the cause is identical every time: input was treated as part of a command instead of as plain text.

    Attackers often hide the second command so it slips past a quick glance in logs or a filter, wrapping it in base64 or another layer of encoding before the shell decodes and runs it. When you are staring at a suspicious payload like that, our free encoded payload deobfuscator peels back common encodings so you can read what the command was actually going to do.

    Why command injection is so serious

    With SQL injection, an attacker reaches your database. With command injection, the attacker reaches the operating system itself, running as whatever user your app runs as. That is a wider blast radius. Once they can run shell commands on your server, they can:

    • Read files the app can read, including config files and secrets like API keys and database passwords.
    • Reach other machines on the internal network that the server can talk to but you cannot reach from outside.
    • Install a backdoor or a reverse shell so they can come back later.

    A field meant to hold a hostname turned into full control of a server. That is why this bug class sits near the top of every serious security list.

    How to fix command injection

    The strongest fix is to stop building shell command strings out of user input. Most of the time you do not need a shell at all.

    Do not shell out when an API exists

    If you only need to read a file, use the file API in your language. If you need to make an HTTP request, use an HTTP library. Reaching for a shell command to do a job your language already does is the start of most of these bugs. No shell means no shell injection.

    If you must run a program, pass arguments as a list

    When you genuinely need to run an external program, call it directly and pass each argument as a separate list item instead of as one big string. Most languages support this. In Python it looks like this:

    # Safer: no shell, arguments passed as a list
    import subprocess
    host = request.form["host"]
    output = subprocess.run(
        ["ping", "-c", "1", host],
        capture_output=True, text=True
    ).stdout
    

    Here host is handed to ping as a single argument. There is no shell to interpret the semicolon, so example.com; whoami is passed to ping as one odd hostname, which fails to resolve. The second command never runs.

    Validate input with an allowlist

    Defense in depth helps too. Decide exactly what valid input looks like and reject everything else. For a hostname, you can allow only letters, digits, dots, and hyphens, and reject anything else before the value goes near a command:

    import re
    host = request.form["host"]
    if not re.fullmatch(r"[A-Za-z0-9.-]+", host):
        return "Invalid host", 400
    

    An allowlist describes what you accept. A blocklist tries to list every bad character and always misses some. Prefer the allowlist.

    Lower the impact when things go wrong

    Run the app as a low privilege user, not as root. Limit what that user can read and which machines it can reach. None of this fixes the bug, but it shrinks the damage if one slips through. You can read more patterns like this in our guide to injection and input bugs.

    How to spot it in your own code

    Search your codebase for the places where commands get run. Look for os.system, os.popen, subprocess calls with shell=True, backticks, exec, and eval. For each one, ask a single question: does any part of this command come from a request, a form, a URL, a header, or a file an outside user can influence? If yes, treat it as suspect and fix it with the steps above.

    Command injection survives because the dangerous code reads as harmless. Joining a string and running it looks fine in review. The bug only shows when someone tries the input you did not expect. This is exactly the kind of assumption an autonomous researcher that tests how an app really behaves is built to find. To see how we think about bugs like this, read more about UnboundCompute.

    Frequently asked questions

    What is command injection?

    It is a bug where user input flows into a system command that the server runs in a shell, so the user can append their own command and make the server run it. The root cause is mixing data, the value a user typed, with code, the command the server runs. See the OWASP command injection guide for more.

    How is command injection different from SQL injection?

    SQL injection reaches your database, while command injection reaches the operating system itself, running as whatever user the app runs as. That is a wider blast radius, because an attacker who can run shell commands can read config files and secrets, reach internal machines, and install a backdoor.

    How do you prevent command injection?

    The strongest fix is to avoid building shell command strings from user input at all, since most jobs have a direct API in your language. If you must run a program, call it directly and pass each argument as a separate list item, for example subprocess.run(["ping", "-c", "1", host]) in Python, so no shell interprets the input. Add an allowlist for the input and run the app as a low privilege user to limit damage.

    How do I find command injection in my own code?

    Search for places that run commands, such as os.system, os.popen, subprocess calls with shell=True, backticks, exec, and eval. For each one, ask whether any part of the command comes from a request, form, URL, header, or file an outside user can influence, and if so treat it as suspect. The matching weakness is tracked as CWE-78.


    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, a say in what it looks for, and founding pricing. If your team ships software worth pressure testing, apply to the design partner program.

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

  • What is web application security?

    What is web application security?

    Web application security is the practice of keeping the apps people use in a browser, and the APIs behind them, safe from misuse. It covers how an app handles input, who is allowed to do what, how it confirms who you are, how it is set up, and whether its business rules hold under pressure. If you are new to the topic, this is a friendly map of what web application security means and why it matters.

    What is web application security?

    An app does a lot of trusting. It trusts that a logged in user only requests their own data. It trusts that a price field really holds a number. It trusts that a hidden form value was not changed. Web application security is the work of checking those assumptions before an attacker does. When one of them is wrong, you get a bug that lets someone read another person’s records, skip a payment step, or run a query they were never meant to run.

    People sometimes ask “what is application security” as if it were one wall around the app. It is closer to many small checks spread across every request. A single weak check is enough. So the goal is not one strong defense, it is consistent ones.

    Why it matters

    Most apps now hold something worth taking: account data, messages, files, money movement, internal tools. The app is also the part of a system most exposed to the open internet. A mistake in one endpoint can reach real users in minutes. That is why teams treat security as part of building the app, not a step bolted on at the end.

    The strongest bugs come from understanding what an app assumes, then proving one of those assumptions is wrong.

    The main risk areas

    You do not need to memorize a long list of attack names to start. Most real issues fall into a handful of groups. Learn these groups and you can reason about a feature you have never seen before.

    Input handling

    An app reads input from forms, URLs, headers, and API bodies. Trouble starts when that input is passed into another system without care. A search box that drops raw text into a database query can become SQL injection. A comment field that echoes raw text back into the page can become cross site scripting. The fix is the same idea each time: treat input as data, never as code.

    POST /api/search
    { "q": "laptop' OR '1'='1" }

    If that q value reaches the database as part of the query string instead of a bound parameter, the trailing condition can change what rows come back. A parameterized query keeps the value as a value.

    Access control

    Access control answers one question: is this user allowed to do this thing, on this object, right now. It is the most common place apps go wrong. Picture an order page:

    GET /api/orders/1042

    If the server returns order 1042 just because you are logged in, and not because order 1042 is yours, then changing the number to 1041 hands you someone else’s order. This is called an insecure direct object reference. The lesson is plain: check ownership on the server for every request, not just in the menu the user sees. We go deeper on this in vulnerability basics.

    Authentication

    Authentication is how the app confirms you are who you claim to be. Weak points include passwords with no rate limit on guessing, session tokens that never expire, password reset links that can be reused, and tokens that leak in a URL. If those session tokens are JSON Web Tokens, our free JWT security inspector decodes one and flags a missing expiry, a weak algorithm, or secrets left in the payload. Authentication decides identity. Access control then decides what that identity may do. They are separate jobs and both must be right.

    Configuration

    Plenty of bugs are not in the code at all. They live in settings. A debug mode left on in production. An admin panel reachable without a login. Default credentials no one changed. An S3 bucket set to public. A verbose error page that prints a stack trace to anyone who triggers it. Configuration review asks a simple question for each setting: what does an outsider see, and is that what we intended. For the security headers an outsider sees on every response, our free security headers and CSP analyzer grades them in seconds.

    Business logic

    The last group is the trickiest because the code can be correct and the app can still be wrong. Logic flaws break the rules of the business, not the syntax of the language. An example:

    • A checkout applies a discount code. It never checks whether that code was already used.
    • So you apply the same code many times and drive the total to zero.
    • Every request is well formed. No injection, no broken auth. The flow just allows a thing it should forbid.

    Scanners rarely catch these, because there is no bad character to flag. You have to understand what the feature is for, then ask what happens at the edges: negative quantities, repeated steps, steps done out of order, two requests racing at once.

    How testing works at a high level

    Testing a web app for security is not one tool you run once. It is a few methods that fit together, each good at finding a different kind of problem.

    Static and dependency review

    Read the source and scan it for risky patterns: raw string queries, missing ownership checks, secrets committed to the repo. Separately, check the libraries the app pulls in, since a known flaw in a dependency is your flaw too. For the third party scripts you load from a CDN, pinning each file with a Subresource Integrity hash stops a tampered copy from running, and our free Subresource Integrity hash generator produces that attribute for you. This is cheap and catches a real share of issues early.

    Dynamic testing

    Run the app and send it crafted requests to watch how it responds. Change an ID. Drop a quote into a field. Replay a request without a login. Send a step out of order. The point is to learn how the app behaves when input does not match what the developer expected.

    Manual and assumption based testing

    A person, or an autonomous tester, studies how the app is meant to work, then forms ideas about where the logic could break, then designs a small experiment for each idea and proves the result with hard evidence. This is where the access control and logic bugs above tend to surface, because finding them needs an understanding of the app, not a fixed list of payloads.

    A note on proof. A guess that an endpoint “might” be broken is not useful. A confirmed finding, shown with a concrete request and response, is. Once a bug is verified, you can turn it into a repeatable check that watches for the same bug returning later.

    Where to go next

    Web application security is a wide field, but it starts with one habit: look at every assumption an app makes and ask what happens when it is false. Pick one risk area, find it in an app you know, and trace it through. That is the kind of bug an autonomous researcher that tests assumptions, not just known payloads, is built to find and verify. If you want to see how that approach works, read more about UnboundCompute.

    Frequently asked questions

    What is web application security in simple terms?

    It is the practice of keeping browser based apps and the APIs behind them safe by checking the assumptions an app makes before an attacker does. Most work falls into a few areas: input handling, access control, authentication, configuration, and business logic. A useful starting map is the OWASP Top 10.

    What are the main types of web application vulnerabilities?

    They group into a handful of families: input bugs like injection and cross site scripting, access control flaws such as reading another user’s record by changing an id, weak authentication, risky configuration like a debug mode left on, and business logic flaws where every request is valid but the flow allows something it should forbid. Learning the groups lets you reason about a feature you have never seen.

    Why do scanners miss business logic bugs?

    A scanner flags bad characters and known patterns, but a logic flaw has no bad character to catch. The code can be syntactically correct and the app still wrong, for example a discount code that can be reused to drive a total to zero. Finding these needs an understanding of what the feature is for, then testing the edges like repeated steps or steps done out of order.

    How do teams test a web app for security?

    They combine methods rather than running one tool. Static and dependency review reads the source and checks libraries, dynamic testing sends crafted requests to a running app, and assumption based testing studies how the app should work and then proves each idea with a concrete request and response. A confirmed finding with a reproduction is far more useful than a list of maybes.


    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, a say in what it looks for, and founding pricing. If your team ships software worth pressure testing, apply to the design partner program.

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

  • Broken object level authorization and IDOR, with examples

    Broken object level authorization and IDOR, with examples

    Broken object level authorization is the most common serious flaw in modern APIs, and it is easy to introduce by accident. The bug is simple to state: the server hands back an object because the request asked for it, without checking that the caller is allowed to see that specific object. This post explains broken object level authorization and its close cousin IDOR, shows a worked API example, and covers how to find the bug and how to fix it.

    What is broken object level authorization

    An API endpoint usually identifies a thing by an id. You ask for order 1001, the server looks up order 1001, and it returns the data. The missing step is the check that this order belongs to you. When that check is absent, any logged in user can read or change objects that belong to other users just by naming their ids. That is broken object level authorization.

    What is IDOR

    IDOR stands for insecure direct object reference. It is the older name for the same idea. A direct object reference is when the id in the request maps straight to a record in the database, like a row primary key. The reference is insecure when the server trusts it without an ownership check. So IDOR describes the exposed id, and broken object level authorization describes the missing check behind it. In practice people use the two terms for the same class of bug.

    The id in the URL tells the server which object to fetch. It must never decide who is allowed to fetch it.

    A broken object level authorization example

    Take an invented app, Acme Notes, that lets people place orders. A signed in user opens their order history and the browser calls this endpoint.

    GET /api/orders/1001
    Authorization: Bearer eyJhbGc...tokenForUserA
    
    200 OK
    {
      "id": 1001,
      "user_id": 42,
      "total": "38.00",
      "shipping_address": "12 Oak Street, Apt 4",
      "items": ["Notebook", "Pen set"]
    }

    The user owns order 1001, so this response is correct. Now they change one digit in the URL and send the same token.

    GET /api/orders/1002
    Authorization: Bearer eyJhbGc...tokenForUserA
    
    200 OK
    {
      "id": 1002,
      "user_id": 77,
      "total": "210.00",
      "shipping_address": "98 Pine Avenue",
      "items": ["Desk lamp", "Monitor stand"]
    }

    Order 1002 belongs to user_id 77, a different person. User A is still authenticated, and the server still returned the record. The token proved who the caller is. Nothing proved the caller owns this order. That gap is the whole bug. By walking the ids from /api/orders/1000 upward, the same caller can read every order in the system, including names, addresses, and totals.

    It is not only reads

    The same gap applies to writes. If the app exposes PATCH /api/orders/1002 or DELETE /api/orders/1002 with no ownership check, a user can edit or delete another person’s order. A profile endpoint like PUT /api/users/77/email with the same flaw lets an attacker take over an account by changing its recovery email. The id can also live in a request body or a query string, not just the path, so {"invoice_id": 1002} deserves the same scrutiny as a URL.

    Why APIs are especially prone to this

    Server rendered web apps often built one page that already filtered records to the current user. APIs split that into many small endpoints, and each one fetches objects by id on its own. Every endpoint becomes a separate place where the ownership check can be forgotten. A few reasons this class of bug keeps appearing:

    • Object ids are visible and guessable. Sequential integers like 1001 and 1002 advertise that 1003 exists. Even random ids do not fix the bug, they only make it harder to find by guessing.
    • The check is per object, not per route. Login and role checks happen once at the edge. Object ownership has to be checked on every single fetch, and it is easy to miss one endpoint out of fifty.
    • Frameworks do not add it for you. Most routing layers confirm the user is logged in. Very few know that order 1002 must belong to the caller. That logic is yours to write.
    • Nested and indirect references multiply the surface. Endpoints like /api/users/42/orders/1001/items/9 have several ids, and a check on one does not cover the others.

    How to detect it

    You find this bug by behaving like a real user with two accounts, then asking whether one account can reach the other’s objects.

    • Create two test users. Sign in as user A and as user B. Note the object ids that belong to each.
    • Swap the ids. While logged in as A, request B’s objects: change /api/orders/1001 to B’s /api/orders/1002 with A’s token. A correct server returns 403 Forbidden or 404 Not Found. A 200 OK with B’s data is the finding.
    • Repeat for every verb. Try GET, then PATCH, PUT, and DELETE on the same id. Read access and write access fail separately.
    • Check ids in every position. Path, query string, JSON body, and headers can all carry an object reference.
    • Watch for indirect leaks. A list endpoint, a search result, or an export job can hand back objects the caller should not see, even when the direct fetch is locked down.

    This is hard to catch with a scanner that only matches known payloads, because there is no payload. The request is well formed and the id is valid. Finding it means understanding what each object is, who should own it, and then testing that assumption directly. More on access control bugs is here.

    The fix: authorize every object on the server

    The cure is one rule applied everywhere: before returning or changing an object, confirm the authenticated caller is allowed to act on that exact object. Do this on the server, in the data layer, not in the client.

    The earlier example is fixed by scoping the lookup to the caller. Instead of fetching by id alone, fetch by id and owner together.

    def get_order(order_id, current_user):
        order = db.orders.find_one(
            id=order_id,
            user_id=current_user.id,   # ownership is part of the query
        )
        if order is None:
            return Response(status=404)
        return Response(order)

    Now order 1002 is invisible to user A, because the query asks for an order with that id that also belongs to A. Some practices that make this reliable across a whole codebase:

    • Scope queries to the owner by default. Filter by tenant or user id in the data access layer so an unscoped lookup is the exception, not the norm.
    • Centralize the check. Put authorization in one policy function each endpoint calls, so the rule is written once and reused, not copied and forgotten.
    • Return 404 for objects the caller cannot access. A 403 confirms the object exists. A 404 reveals less.
    • Do not rely on hard to guess ids alone. Random UUIDs reduce guessing, but the server must still check ownership. Obscurity is not authorization.
    • Write a test per endpoint. For each object route, add a test where user A requests user B’s object and asserts the response is denied. This keeps the bug from coming back.

    Broken object level authorization is a logic bug, not a string in a payload. Finding it means knowing what each object is, who should own it, and then 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 broken object level authorization?

    Broken object level authorization is when the server hands back an object because the request asked for it, without checking that the caller is allowed to see that specific object. Any logged in user can then read or change objects that belong to other users just by naming their ids. See the OWASP API Security entry on this flaw.

    What is the difference between IDOR and broken object level authorization?

    They describe the same class of bug from two angles. IDOR, insecure direct object reference, is the older name and points at the exposed id that maps straight to a database record, while broken object level authorization names the missing ownership check behind it. In practice people use the terms interchangeably.

    How do you test for an IDOR or broken object level authorization bug?

    Create two test users, then while signed in as user A request user B’s objects by swapping the id, for example changing /api/orders/1001 to B’s /api/orders/1002 with A’s token. A 403 or 404 is correct, while a 200 OK with B’s data is the finding. Repeat for GET, PATCH, PUT, and DELETE, since read and write access fail separately.

    How do you fix broken object level authorization?

    Before returning or changing an object, confirm the authenticated caller is allowed to act on that exact object, on the server and in the data layer. Scope the lookup to the owner by fetching by id and user id together, centralize the check in one policy function, and do not rely on hard to guess ids alone since obscurity is not authorization.


    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, a say in what it looks for, and founding pricing. If your team ships software worth pressure testing, apply to the design partner program.

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