Security Testing Basics
A shopping app can pass every functional test and still let one customer read another customer's orders, or let an attacker log in as the admin. Security testing looks for weaknesses that could let someone steal data, misuse features or take a system down. Every tester should know the basics, even in teams that also have dedicated security specialists.
In this lesson you will learn the rules for testing safely and legally, the OWASP Top 10 list of common web risks, and how to write security test cases for injection, cross-site scripting and access control against your own code.
The OWASP Top 10
OWASP (the Open Worldwide Application Security Project) is a non-profit community that publishes free security guidance. Its best-known document, the OWASP Top 10, lists the most critical web application security risks. The current edition is the 2025 list:
| Code | Risk | A tester's question |
|---|---|---|
| A01:2025 | Broken Access Control | Can a user see or change data that is not theirs? |
| A02:2025 | Security Misconfiguration | Are debug pages, default passwords or detailed errors exposed? |
| A03:2025 | Software Supply Chain Failures | Are libraries and build tools up to date and from trusted sources? |
| A04:2025 | Cryptographic Failures | Are passwords hashed and data sent only over HTTPS? |
| A05:2025 | Injection | Can input change a database query or command? |
| A06:2025 | Insecure Design | Is the feature unsafe by design, such as unlimited OTP attempts? |
| A07:2025 | Authentication Failures | Are weak passwords, endless login attempts or stolen sessions possible? |
| A08:2025 | Software or Data Integrity Failures | Can updates or data be tampered with unnoticed? |
| A09:2025 | Security Logging and Alerting Failures | Would an attack be recorded and noticed? |
| A10:2025 | Mishandling of Exceptional Conditions | Do errors fail safely, or do they leak data or skip checks? |
You do not need to be a security expert to test many of these. Several are ordinary test cases with a security mindset.
Injection: testing your own login
SQL injection happens when code builds a database query by pasting user input into SQL text. A quote character in the input then changes the meaning of the query. The fix is a parameterised query, where the database receives the input separately from the SQL. This example tests two versions of a login function in an in-memory database that the script itself creates:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE users (email TEXT, password TEXT, role TEXT)")
db.execute("INSERT INTO users VALUES ('admin@kart.test', 'S3cret!', 'admin')")
db.execute("INSERT INTO users VALUES ('priya@kart.test', 'Kart@2026', 'customer')")
def login_unsafe(email, password):
query = f"SELECT role FROM users WHERE email = '{email}' AND password = '{password}'"
return db.execute(query).fetchone()
def login_safe(email, password):
query = "SELECT role FROM users WHERE email = ? AND password = ?"
return db.execute(query, (email, password)).fetchone()
# Security test cases for our OWN login function
cases = [
("normal login", "priya@kart.test", "Kart@2026", ("customer",)),
("wrong password", "priya@kart.test", "guess", None),
("quote in input", "admin@kart.test' --", "anything", None),
]
for label, email, password, expected in cases:
for fn in (login_unsafe, login_safe):
try:
result = fn(email, password)
except sqlite3.Error as error:
result = f"database error: {error}"
verdict = "PASS" if result == expected else "FAIL"
print(f"{verdict} {fn.__name__:<12} {label}: {result}")PASS login_unsafe normal login: ('customer',)
PASS login_safe normal login: ('customer',)
PASS login_unsafe wrong password: None
PASS login_safe wrong password: None
FAIL login_unsafe quote in input: ('admin',)
PASS login_safe quote in input: NoneBoth versions pass the normal functional tests. Only the security test case shows that the unsafe version lets a quote and the SQL comment marker -- skip the password check and return the admin role. The parameterised version treats the whole input as an ordinary email that matches nobody. When you find this in a real product, report it as Critical and never "try more" on live data.
Cross-site scripting (XSS)
Cross-site scripting happens when a page shows user input as HTML instead of text, so script code typed by one user runs in other users' browsers. The defence is to escape output (and, in templates, to use frameworks that escape by default). A test case enters harmless markup and checks that it appears as text:
import html
def render_comment_unsafe(name, text):
return f"<p><b>{name}</b>: {text}</p>"
def render_comment_safe(name, text):
return f"<p><b>{html.escape(name)}</b>: {html.escape(text)}</p>"
payload = '<img src=x onerror="alert(1)">'
for render in (render_comment_unsafe, render_comment_safe):
output = render("Kabir", payload)
verdict = "FAIL: markup injected" if "<img" in output else "PASS: shown as text"
print(f"{render.__name__}: {verdict}")
print(" ", output)render_comment_unsafe: FAIL: markup injected
<p><b>Kabir</b>: <img src=x onerror="alert(1)"></p>
render_comment_safe: PASS: shown as text
<p><b>Kabir</b>: <img src=x onerror="alert(1)"></p>In the escaped version, < becomes < and the browser shows the characters instead of creating an image element. In a manual test you would type such text into a comment box of your own test environment and check that it is displayed literally.
Access control: the most common risk
Broken access control tops the OWASP list. The typical case: a customer opens /api/orders/1042 (their order), changes the number to 1043 and sees someone else's name, address and phone. Testers find this with simple API tests that use two test accounts:
# test_access_control.py - run only against your own test server
import os
import requests
BASE = os.environ["STAGING_URL"]
def login(email, password):
r = requests.post(f"{BASE}/api/login", json={"email": email, "password": password}, timeout=10)
return {"Authorization": f"Bearer {r.json()['token']}"}
def test_customer_cannot_read_another_customers_order():
priya = login("priya@kart.test", os.environ["PRIYA_PASSWORD"])
rohan_order_id = os.environ["ROHAN_ORDER_ID"] # created for this test
r = requests.get(f"{BASE}/api/orders/{rohan_order_id}", headers=priya, timeout=10)
assert r.status_code in (403, 404)
def test_customer_cannot_open_admin_api():
priya = login("priya@kart.test", os.environ["PRIYA_PASSWORD"])
r = requests.get(f"{BASE}/api/admin/users", headers=priya, timeout=10)
assert r.status_code == 403Other quick checks with a security mindset: log out and press Back, call an API without a token, try ten wrong OTPs (is there a limit?), look at error pages for stack traces, and check that every page uses HTTPS.
Tools
- Dynamic scanners such as ZAP (open source) and Burp Suite crawl a running test site and probe it for common weaknesses.
- Static analysis (SAST) tools such as Semgrep, CodeQL or Bandit (for Python) read source code for risky patterns.
- Dependency scanners such as
pip-audit,npm auditand GitHub's Dependabot report libraries with known vulnerabilities, which addresses supply chain risks. - Browser developer tools show cookies, headers and network calls, and are enough for many manual checks.
Scanners produce false alarms and miss logic flaws such as broken access control, so treat their output as leads to verify, not as the whole test.
Common mistakes
- Testing outside your scope. Get written permission and stay inside the agreed systems and dates.
- Only testing the user interface. Many access-control bugs are only visible when you call the API directly.
- Real personal data in security tests. Use dedicated test accounts and synthetic data.
- Sharing exploit details in public bug trackers. Report security bugs through the agreed private channel.
Key points
- Test only systems you own or have written permission to test; practise on apps like OWASP Juice Shop or DVWA.
- The OWASP Top 10 (2025) lists the most critical web risks, led by broken access control.
- Parameterised queries prevent SQL injection; escaping output prevents cross-site scripting.
- Access-control tests use two accounts to check that users cannot reach each other's data or admin functions.
- Scanners, static analysis and dependency checks help, but manual thinking finds logic flaws.
Exercise
Write a function can_view_order(user, order) for your own shop code, where user and order are dictionaries. Admins may view any order; customers may view only orders whose customer_id equals their own id. Write four checks: owner, other customer, admin, and a user with no role.
Show solution
def can_view_order(user, order):
if user.get("role") == "admin":
return True
if user.get("role") == "customer":
return user.get("id") == order["customer_id"]
return False
order = {"id": 1043, "customer_id": 7}
checks = [
("owner", {"id": 7, "role": "customer"}, True),
("other customer", {"id": 8, "role": "customer"}, False),
("admin", {"id": 1, "role": "admin"}, True),
("no role", {"id": 7}, False),
]
for label, user, expected in checks:
result = can_view_order(user, order)
print("PASS" if result == expected else "FAIL", label, result)PASS owner True PASS other customer False PASS admin True PASS no role False
The last check matters: the function denies by default, so a user with a missing or unknown role gets no access. Allowing access unless something forbids it is a classic cause of broken access control.
Quick Quiz
1. Which risk is A01 in the OWASP Top 10:2025?
Broken access control is first in the 2025 list.
2. What prevents SQL injection in the login example?
With placeholders, the database never treats input as part of the SQL command.
3. A customer changes an order ID in the URL and sees another customer's order. Which risk is this?
The server failed to check that the order belongs to the logged-in user.
4. Which is a safe and legal way to practise security testing?
Practise only on systems you own or are authorised to test, such as deliberately vulnerable training apps.
Learn Software Testing with a mentor
Free lessons get you started. Our Software Testing training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
