# How to Test Broken Object-Level Authorization with Two Logins

Canonical: https://vulnify.app/blog/test-bola-with-two-logins

Broken object-level authorization is an ownership failure. Use two accounts you control, and learn what a Vulnify login profile checks automatically versus what you still have to confirm by hand.

Broken object-level authorization means a signed-in user can open, change, or download a record that belongs to someone else by changing an identifier. The login succeeded. The ownership check did not. That is different from a missing login, and different from a role that should never see an admin screen. Testers mix the three up, then either miss the bug or file the wrong ticket. This article is the concrete test: two accounts you control, and sample records those accounts own. Attaching a login to a scan is covered in how to scan pages that require a login . Tenant isolation as a design topic is a wider conversation. The check below is the narrow one a scanner can help with, and the part it cannot finish. Separate authentication, ownership, and role Authentication asks whether the request is tied to a known user. If you are sent to a login page, you have not started an ownership test. Ownership asks whether this user may touch this object. The URL already contains the id, or the JSON body does. Role permission asks whether this kind of user may use this function at all, such as “editors cannot open billing.” A user who can open their own invoice and also the next invoice number is an ownership bug even when their role is allowed to see invoices. Write the expected result before you test. Account A must get A’s order and must not get B’s order. Account B must get B’s order and must not get A’s. An administrator might be allowed to see both. If you do not write that down, a 200 response feels like success when it is the bug. Create two boring accounts and two records Use accounts you created for the test, on a system you are allowed to test. Give them the same role if you are testing horizontal access, which is the usual object-level case. Give them different roles only when the question is the role gate, and label that question so it does not get mixed into the object test. Create one private record as each user: an order, a document, a message, a project. Copy the URLs or API paths while you are logged in as the owner. Prefer records that are not public catalog pages. A product page that every shopper can open is not an ownership test. Log in as A and request B’s URL. Then log in as B and request A’s URL. A correct application returns a denial, or a generic not-found that does not include B’s data. A failure returns B’s data, a file, or a JSON body that only B should see. Repeat for edit and delete if those actions exist. Reading someone else’s record and changing it are the same class of bug with different impact. What Vulnify automates An authenticated session add-on runs probes with one saved login. Those probes look for numeric identifiers in the path, or in query parameters named id , userId , user_id , caseId , or patientId . They send GET requests that swap nearby numbers. They skip paths that look like public content, including blog, shop, and product pages. A hit is an alternate id that returns HTTP 200 with a body that differs from the baseline. The sample size is 10 ids, or 25 when aggressive active probes are also on. A second saved profile repeats the comparison. Vulnify loads both sessions and probes the same discovered object URLs with each. A dual-auth result means both sessions could access that object URL. It does not walk an admin menu, and it does not prove that the body was the other account’s private record. Someone still has to open the evidence and see whose data came back. UUIDs, ids that live only in a POST body, and GraphQL node ids are outside this GET swap. If the crawl never saw the object URL, the probe has nothing to swap. What you still test by hand After the automated pass, take the two URLs you copied and exchange them in the browser while each test account is signed in. Look at the body, not only the status code. A 200 with an empty shell, a 200 with the other user’s name, and a 403 are three different outcomes. The scanner’s “body changed” signal can also fire on a page that merely says “hello, other id” without leaking private fields. Confirm the fields. Then try one write. Change a status or a nickname on the other user’s record and see whether it sticks. Do this on the test accounts only. The automated probes described above are GET requests. They will not notice an unsafe update. Storefront order URLs are a poor fit for the automated skip list, because shop and product paths are ignored. If you sell with WooCommerce, treat another customer’s order as a manual check. The 15 checks before you go live do not cover order URLs, and the object-id probe does not either. How to file the result A real finding names the two accounts’ roles, the URL pattern, the id you changed, and what came back. “IDOR on the API” is not a ticket. “Account A, a normal member, received account B’s invoice PDF by changing the numeric id in GET /invoices/18” is a ticket. Fix it by checking, on the server, that the authenticated user owns or is allowed to access that id. Hiding the id in the interface does nothing, because the other id is just the next number. Retest with the same two accounts after the patch. You want a denial for the cross-account URL and a success for the owner’s own URL. Breaking the owner’s access is a failed fix. From finding to verified fix applies here as much as it does to a header change. If you use a pentest, a second profile is what allows the cross-account comparison to run. Without it, that part of the test is skipped and the report should say so. Do not test with real customers The second account should be one you created, with data you are allowed to see. Swapping ids against a live customer, even just to check, copies their data into your screen, your logs, and the scan evidence. Use obvious test names so a later reader of the evidence knows the record was synthetic. Delete the test accounts when the ticket is closed, or keep them only if they are clearly marked and hold no real personal data. If the only way to reach the object is a production admin acting as a customer, stop and build the two test accounts first. A missing test fixture is a reason to delay the test. It is not a reason to browse a stranger's order. Frequently asked questions Is one login enough? One login can show that alternate numeric ids return different 200 responses. It cannot show that those responses belong to a second real account. Two accounts you control are how you prove the cross-account case. Does this test admin versus member? Only if you designed the test that way. The automated swap checks object ids, not whether a role can open a settings screen. Role gates need their own expected results. Does a quiet scan mean authorization is done? No. Quiet means the discovered numeric GET urls did not return a different 200, or no such urls were discovered. Body parameters, UUIDs, and pages the crawl skipped were not cleared.
