All articles

Security and Authentication Testing QA Can Actually Own

I once tested a retail platform that ran two storefronts off one backend. Part of my job was to prove that a shopper on one brand could never reach an order belonging to the other. So I logged in as one user, found the request that pulled up my own order, changed a single number in it to point at someone else’s, and sent it again with my own login still attached.

That one small change is all the test is, and it is one an automated scanner cannot run for you. The request looks completely valid: a real user, a real session, a normal endpoint. The only thing wrong is who the order belongs to, and a scanner has no idea who is supposed to own what. I did, because I knew the data model, so I was the one who could catch it.

That test is what changed my mind about security. For a long time it sounded like someone else’s job to me too: the specialist with the black terminal and the certifications, the annual penetration test that lands as a PDF nobody reads until audit season. So we check that the login form works, confirm the password field is masked, and move on, assuming the scary stuff is handled somewhere upstream.

It usually is not. The bugs that do the most damage are not exotic. A user who reads another user’s order by changing a number in the URL. A session that still works after logout. A password reset that leaks a token. An error page that prints your database query back to the attacker.

None of that needs a specialist. It needs someone who can send a request, read a response, and ask the question QA asks about everything else: where can this break?

Years of testing fintech and e-commerce taught me to ask that of anything touching money or identity, and building a game where a player’s balance and progress lived on a server made it concrete all over again. The moment real money and real data sit behind an endpoint, “is this secure enough to ship” stops being someone else’s question. What you can own is a repeatable security baseline that runs on the same web app and APIs you already test, with the tools you already have open.

Start from where the real risk lives

Anchor on the standard list so you are not guessing at what matters. The OWASP Top 10 ranks the most serious web application security risks.

In the 2025 edition, the first refresh since 2021, the top two are broken access control and security misconfiguration, with a new entry at number three, software supply chain failures. Authentication failures are still on the list, lower down at A07.

Broken access control has held the number one spot for years, and that is no coincidence. It is the bug that is easiest to ship and easiest to miss, because the happy path looks perfect.

If your product has a backend, the list that matters even more is the OWASP API Security Top 10, because a web or mobile front end is mostly a thin shell over an API, and that is where the real authority decisions get made.

Its number one risk is API1:2023 Broken Object Level Authorization (BOLA), the API name for what you may know as Insecure Direct Object Reference, or IDOR. It is the same bug as web broken access control seen from the API side, and the single most valuable thing a QA can learn to test.

Access control sits at the top of both lists. Start there.OWASP Top 10 (web, 2025)#1Broken access controlheld #1 for years#2Security misconfiguration#3Software supply chainOWASP API Top 10 (2023)#1BOLA (object-level)the API name for IDORSame bug, seen from the API side. Theid-swap test below proves it on either.If the happy path looks perfect, look here first.

Security testing is risk thinking with a sharper edge, so it starts the same way the rest of my testing does. Run the feature through these before you open a single tool:

The questions I ask before I sign off on auth:

  • Can user A reach user B’s data by changing an id in the request?
  • What happens with no token, an expired token, and a token for the wrong role?
  • Which actions check permission on the server, not just hide a button on the screen?
  • Is any sensitive data sitting in a URL, a log, or a response where it should not be?
  • And the one that scopes the rest: what is the worst a motivated user could do here?

I am not going to walk the whole top ten. I am going to give you a baseline you can actually run, in the order I would test it:

  • Access control (IDOR / BOLA, and vertical privilege escalation)
  • Authentication and sessions (login, lockout, cookies, tokens, logout)
  • Transport and headers (HTTPS only, the security response headers)
  • Secrets and leakage (keys in bundles, error messages that talk too much)
  • Input (injection probes for SQL injection and cross-site scripting)

Each is a question you can answer with a proxy, Postman, your browser’s developer tools, and a bit of patience.

Access control: the test only QA tends to actually run

That two-brand test is the one to reach for first, and it is worth being clear about why it lands on QA. Automated scanners are bad at broken access control, because the request looks completely valid: the right user, a real session, a normal endpoint. The only thing wrong is that the object on the other end belongs to someone else, and a scanner has no idea who is supposed to own what.

You do. You know the data model, and you know user A should never see user B’s order, so you are the one who can prove it.

The method is simple enough to memorise.

1. Log in as User A, capture a request you ownGET /api/orders/1001 Authorization: Bearer A2. Swap in User B’s id, keep User A’s tokenGET /api/orders/1002 Authorization: Bearer A3. Read the responseSecure403 Forbidden or 404 Not FoundBug200 OK returning User B’s data

Log in as user A and capture a request that references an object you legitimately own, for example GET /api/orders/1001. Now swap in user B’s object id, 1002, while still authenticated as A, and send it again.

A server that gets access control right answers 403 Forbidden or 404 Not Found. A server that gets it wrong answers 200 OK and hands you user B’s order. That 200 is the bug, and one of the most common serious findings there is.

Three things turn this from a one-off poke into real coverage:

  1. Repeat it across the verbs. A GET can be locked down while a PUT or a DELETE is wide open, because someone added the read check and forgot the others. Try reading, editing, and deleting another user’s object. A user who cannot view user B’s order but can delete it is arguably worse off.
  2. Test the no-token and bad-token cases too. Send the same request with no Authorization header, then with an expired token. Both must be rejected. It is surprisingly common for an endpoint to enforce ownership for a logged-in user but happily answer an unauthenticated request.
  3. Try going up, not just sideways. That is vertical privilege escalation, what OWASP calls Broken Function Level Authorization (BFLA). Take a normal user’s token and call an admin-only endpoint, say DELETE /api/admin/users/55 or GET /api/admin/reports. You should get 403. Hiding the admin button in the UI is not access control. The endpoint is still there, and the front end is not what stops anyone.

In practice the check looks like this. Request your own object and you are allowed; keep your own token but swap the id to someone else’s, and you must be refused:

# Your own object: allowed
GET /v1/orders/8412   Authorization: Bearer <your-token>
→ 200 OK   (your order)

# Same request, your token, someone else's id: must be refused
GET /v1/orders/9001   Authorization: Bearer <your-token>
→ 403 Forbidden   { "type": "about:blank", "title": "Forbidden", "status": 403 }

The pass condition is that second 403. If the swapped request ever comes back 200, you have found a real authorization hole, and that is exactly the bug this test exists to catch.

Burp Suite Community Edition is my tool of choice for this. Browse as user A with Burp’s proxy on, find the request in the history, send it to Repeater, change the id, and resend, watching the status code and body change in real time.

Community Edition has Repeater and the proxy, which is all you need here. It lacks the automated active scanner, which is the paid feature, and for access control the scanner would not help you anyway.

OWASP ZAP does the same job, fully free and open source, with a passive scanner that flags other issues while you work. And once you have proven a swap by hand, automate it, which is where Postman earns its place.

Automate the access-control check in Postman

Manual proof is the start. What protects you over time is a saved request with assertions, so it runs on every regression pass instead of living in your memory. In Postman this goes in the Tests tab (the Scripts tab in newer versions), which runs after the response comes back.

The setup: one request that hits user B’s object, authenticated with user A’s token. Put user A’s token in a collection or environment variable so it is easy to swap. The test is then a single, readable assertion.

Postman
// Tests tab. Request: GET {{baseUrl}}/api/orders/1002
// Auth: Bearer {{userA_token}}  (a token that belongs to User A, not User B)

pm.test("User A cannot read User B's order", function () {
  pm.expect(pm.response.code).to.be.oneOf([403, 404]);
});

// Catch the silent leak explicitly: a 200 here is the finding.
pm.test("Response does not expose another user's data", function () {
  if (pm.response.code === 200) {
    const body = pm.response.json();
    pm.expect.fail(
      `Expected 403/404 but got 200 returning order owner ${body.userId}`
    );
  }
});

Then duplicate the request for the negative-auth cases, because those are part of the same control:

Postman
// Same endpoint, but send NO Authorization header, then an EXPIRED token.
pm.test("Unauthenticated request is rejected", function () {
  pm.expect(pm.response.code).to.be.oneOf([401, 403]);
});

Group these into a “negative auth” folder in your collection, point Newman or your continuous integration (CI) job at it, and broken access control becomes a thing your pipeline catches, not a thing you hope someone remembers to check by hand.

For the wider structure of this kind of API coverage, I keep a API testing checklist you can build the suite from, and the API and microservices testing guide goes deeper on testing the endpoints and their contracts.

Authentication and sessions: test the whole lifecycle, not the login screen

It is easy to test that login works. The bugs live in everything around it. The OWASP Web Security Testing Guide (WSTG) is the practical manual here, readable even if you have never opened a security document before. These are the checks I run.

Credentials never travel over plain HTTP. Watch the login request in your developer tools or proxy. It must be HTTPS. A login form that posts over HTTP, even one that “redirects to HTTPS after”, has already exposed the password on the way in.

Brute force has a ceiling. Hammer the login with wrong passwords. There has to be account lockout, rate limiting, or both, or someone can guess forever. The WSTG calls this out under its authentication tests, and the absence of any throttling is a real finding.

Password policy follows current guidance, not folklore. This surprises people, because the old rules are wrong now. NIST Special Publication 800-63B sets the modern bar, and as a tester you check that the product matches it:

  • a minimum of at least 15 characters, and at least 64 allowed (long passphrases must be accepted, not truncated)
  • no forced composition rules (do not require a mix of upper, lower, number, and symbol)
  • no forced periodic rotation (expiring passwords every 90 days is now discouraged, not required)
  • new passwords screened against a breached-password blocklist, so a known-compromised password is refused

If the product still demands one uppercase, one symbol, and a change every quarter, that is not extra safety, it is a finding against the current standard.

Sessions are wired correctly. This is where most session bugs hide, and your developer tools show all of it. Open the Application or Storage tab and check the session cookie:

  • HttpOnly is set, so JavaScript cannot read the cookie and a cross-site scripting bug cannot steal it
  • Secure is set, so it is only ever sent over HTTPS
  • SameSite is set (Lax or Strict), which blunts cross-site request forgery
  • logging out actually invalidates the session on the server, not just clears it in the browser. Capture a valid session token, log out, then replay a request with that old token. It must be rejected. If the old token still works, logout is a lie.
  • the session times out after a sensible idle period
  • you cannot fix a session: a session id handed to you before login must not still be valid, now elevated, after you log in (that is session fixation)

If it uses tokens, test the token. Plenty of backends issue a JSON Web Token (JWT, RFC 7519). You do not need to be a cryptographer. Paste it into your developer tools and try three things the server must refuse:

  • set its header alg to none, strip the signature, and send it. A server that accepts an unsigned token is critically broken, because anyone can then mint a token for anyone.
  • change a single character in the payload (flip your user id, or "admin": false to true) and send it. The signature no longer matches, so it must be rejected.
  • send an expired token, one past its exp claim. It must be rejected, not quietly honoured.

None of that authentication baseline needs anything past your browser and a proxy.

Transport and headers: cheap to test, embarrassing to miss

This is the fastest part of the baseline and the one most likely to find something on a real app. Two layers: is the connection encrypted, and is the browser being told how to behave safely.

For transport, confirm the whole site is HTTPS, that plain http:// requests redirect to https://, and that the Strict-Transport-Security header is present so the browser refuses to downgrade.

While you are in the developer tools, watch for mixed content, a secure page pulling a script or image over plain HTTP, which the console will warn you about.

For the response headers, the OWASP Secure Headers project is the reference. Read them straight off the Network tab, or with one curl -I https://yoursite from the terminal. Here is what to look for and a sane example value for each.

HeaderWhat it doesExample value
Strict-Transport-SecurityForces HTTPS; browser refuses to downgrademax-age=63072000; includeSubDomains
Content-Security-PolicyLimits where scripts, styles, and frames can load from; a strong defence against cross-site scriptingdefault-src 'self'
X-Content-Type-OptionsStops the browser guessing a file’s type (MIME sniffing)nosniff
X-Frame-OptionsStops your page being framed by another site (clickjacking)deny
Referrer-PolicyControls how much of the URL leaks to other sitesstrict-origin-when-cross-origin
Permissions-PolicySwitches off browser features the app does not usegeolocation=(), camera=()

A missing header is not always a crisis on its own, but a login or payment flow served without Strict-Transport-Security and a Content-Security-Policy is worth raising every time. These are a few lines of server config, so the fix is cheap once flagged.

Which tool for which check

You will not reach for every tool on every check. Here is how the realistic QA toolkit maps to the baseline, so you know what to open for each job.

CheckOWASP ZAPBurp CommunityPostmanDevTools
IDOR / BOLA id-swap by hand~
Automating the access-control regression~
Inspecting cookie flags and tokens~~
Replaying a request with a tampered token
Checking security response headers~~
Passive scan for low-hanging issues

the natural tool for this   ~ possible but not where it shines   not the right tool

If you install only one thing, make it an intercepting proxy (ZAP or Burp Community), because seeing and replaying live traffic is the skill the rest of this rests on. curl covers the header and token checks from a script, and Postman turns the manual access-control checks into a suite that runs forever.

Secrets and leakage: ask the app to say too much

Two related failures, both common and easy to test. The app is carrying a secret it should not expose, or its errors reveal its insides.

For secrets, the front end is not a safe place to keep anything, because everything shipped to the browser or device can be read. Open the loaded JavaScript bundle in the Network or Sources tab and search it for key, secret, token, and password.

A private API key, a cloud credential, or a signing secret in client code is a real finding, because anyone can read it. Do the same to API responses and error bodies.

In the repository, confirm .env files and key material are not committed, and that automated scanning stops a secret slipping in later. git-secrets scans commits for credential patterns before they land, and TruffleHog digs through a repository’s whole history for leaked keys. Both are quick to wire into CI.

For error leakage, break things on purpose. Send malformed JSON, a string where a number is expected, a wildly oversized payload, an id that does not exist. Then read what comes back.

A safe app returns a generic, controlled error. A leaky one hands you a stack trace, a raw SQL error, an internal file path, or the exact framework and version it runs, all of it a free map for an attacker. The fix is for the server to log the detail privately and return a clean message, and verifying that is squarely a QA job.

Input: the injection probes worth keeping in your pocket

Full injection testing is a deep field, but two quick probes belong in every tester’s baseline because the failures are severe and the checks are simple.

For SQL injection, drop a single quote ' into a field you suspect reaches a database: a search box, a login, a filter. If the page throws a database error, the input is reaching the query unsafely, a finding worth escalating. A safer probe is a classic always-true fragment such as ' OR '1'='1 in a login field. It must fail to log you in.

For cross-site scripting (XSS), submit <script>alert(1)</script> or an <img src=x onerror=alert(1)> into a field whose value gets shown back on a page: a name, a comment, a profile bio. If a dialog pops, the app is rendering your input as live code instead of plain text.

Check both where you typed it and anywhere else that value resurfaces, because stored cross-site scripting shows up on a different screen than the one you submitted on.

These two will not make you a penetration tester, and they are not meant to. They catch the obvious, severe cases early, and tell you where to ask for a deeper look.

This is where AI actually helps, and where it does not

A coding agent like Claude Code is genuinely useful here, as long as you are honest about which parts. It is fast at combing a large API response for a field that should never have been returned, an internal flag, another user’s email, a password hash in the payload, the kind of thing that is tedious to eyeball across hundreds of lines. It is also good at generating variations of injection and fuzzing payloads, faster than you would type them.

What it does not own is the judgment. Whether a 200 actually matters, whether that returned field is a real leak or harmless, whether a finding is worth blocking a release, that is risk thinking, and it stays with you.

AI widens the net; you decide what in the net is a real fish. I have written more on that division of labour in the QA control layer for AI-assisted development, so one line here: the agent helps you look, you decide what it means.

Never trust the client, which is the whole game on mobile

Everything above sharpens on mobile, the most hostile environment there is. It runs on a device you do not control, and a determined user can inspect it, modify it, and lie to your server with it. Anyone who has tested a mobile app with real accounts and purchases has had to assume exactly that.

The OWASP Mobile Application Security Verification Standard (MASVS) is blunt: treat the client as already compromised, and any control you put there as defence in depth only, never as the thing that actually protects you.

In practice the server is the source of truth for the three things that matter: identity, money, and entitlements. A score the client reports, a currency balance the client claims, a “yes this user paid” the client asserts, none of it can be trusted on its own.

The leaderboard score has to be validated server-side or someone will post a billion. The coin balance has to live on the server or someone will edit it locally. And the purchase has to be verified against Apple’s App Store Server API or Google Play’s Developer API on your backend, not taken on the client’s word.

The security lens adds one question to that chain: if I lie to the server from a hacked client, does it believe me? Prove the answer on the server, not on the screen.

Your copy-paste QA security baseline

Here is the whole thing as a checklist, grouped the way I run it, genuinely actionable, and none of it needs a security specialist. Paste it into your test plan, adapt the endpoints to your app, and make it part of your regression and pre-release passes.

Access control

  • Logged in as user A, requesting user B’s object returns 403 or 404, never 200 with B’s data
  • The id-swap is blocked across GET, PUT, and DELETE, not just read
  • The same request with no token is rejected (401/403)
  • The same request with an expired token is rejected
  • A normal user’s token cannot call admin-only endpoints (vertical escalation returns 403)
  • The access-control checks are saved as automated assertions, not done once by hand

Authentication and sessions

  • Credentials are only ever sent over HTTPS
  • Repeated failed logins hit account lockout or rate limiting
  • Password rules match current guidance: long minimum, no forced composition, no forced rotation, breached passwords refused
  • Session cookie has HttpOnly, Secure, and SameSite set
  • Logout invalidates the session server-side (an old token replayed after logout fails)
  • Sessions time out after idle, and session fixation is not possible
  • JWT checks pass: alg: none rejected, tampered payload rejected, expired token rejected

Transport and headers

  • The whole site is HTTPS and plain http:// redirects to https://
  • Strict-Transport-Security is present
  • Content-Security-Policy, X-Content-Type-Options: nosniff, X-Frame-Options, Referrer-Policy, and Permissions-Policy are set
  • No mixed content (secure pages loading insecure resources)

Secrets and leakage

  • Client bundles, API responses, and error bodies contain no keys, tokens, or passwords
  • .env files and key material are not committed; secret scanning runs in CI
  • Forced errors return a clean message, with no stack trace, SQL error, internal path, or framework version

Input

  • A ' in database-backed fields produces no raw database error
  • ' OR '1'='1 in a login field does not authenticate
  • <script>alert(1)</script> is rendered as text, not executed, everywhere the value appears

To keep this from being a one-person heroics exercise, hang an owner and a finish line on the parts that decide whether it actually runs:

  • Owner: QA. Run the access-control id-swap across the verbs and the negative-auth cases. Done when: user A requesting user B’s object returns 403 or 404 on GET, PUT, and DELETE, and no-token and expired-token requests are rejected.
  • Owner: Dev. Add the saved Postman or Newman assertions to CI. Done when: the access-control and negative-auth checks run on every pipeline pass and a 200 leak fails the build.
  • Owner: EM. Decide what blocks a release. Done when: it is written down that a broken-access-control or unsigned-token finding stops the ship, and a missing header is logged rather than gating.

Where to start

You do not have to do all of this on day one, and you should not try. Pick the access-control test first, because it finds the most serious bugs and is the one only QA reliably runs.

Install a proxy, log in as two users, swap one id, and watch what comes back. That single exercise, done on your real product this week, will teach you more about your application’s security than any checklist can, and probably turn something up.

From there, fold the rest in one group at a time, automate the access-control and negative-auth checks so they run on every pass, and let the boring header and token checks ride along in curl.

You are not trying to replace the penetration test. You are catching the bugs that would otherwise reach it, or reach a user, first. Point that “where can this break” instinct at identity, money, and the objects users own, prove it on the server, and you ship with evidence instead of crossed fingers.

Quickest possible start: log in as two test users, capture one request that returns data you own, swap in the other user’s id, and resend it with your own token. If you get a 200 with their data, you have found your first access-control bug, and you found it with tools you already had open.

Found it useful? Share it.
Julia Pottinger

Written by

Julia Pottinger

Hi, I'm Julia. I've been in QA for over a decade. I spend my days testing software and my own time building apps and games, and I write here to share what I learn, the practical, honest lessons you can actually use.

Comments 0

Share your thoughts, ask questions, or add to the conversation.

Be kind and constructive. Stay on topic. No spam or self-promotion.
Loading comments…