EthicsComputerCourses
Join training +91 90532 10052

Security Testing Basics

Software Testing · Lesson 25 of 30 Advanced 7 min read Updated

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:

CodeRiskA tester's question
A01:2025Broken Access ControlCan a user see or change data that is not theirs?
A02:2025Security MisconfigurationAre debug pages, default passwords or detailed errors exposed?
A03:2025Software Supply Chain FailuresAre libraries and build tools up to date and from trusted sources?
A04:2025Cryptographic FailuresAre passwords hashed and data sent only over HTTPS?
A05:2025InjectionCan input change a database query or command?
A06:2025Insecure DesignIs the feature unsafe by design, such as unlimited OTP attempts?
A07:2025Authentication FailuresAre weak passwords, endless login attempts or stolen sessions possible?
A08:2025Software or Data Integrity FailuresCan updates or data be tampered with unnoticed?
A09:2025Security Logging and Alerting FailuresWould an attack be recorded and noticed?
A10:2025Mishandling of Exceptional ConditionsDo 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:

PythonRun it yourself
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}")
Output
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: None

Both 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:

PythonRun it yourself
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)
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>: &lt;img src=x onerror=&quot;alert(1)&quot;&gt;</p>

In the escaped version, < becomes &lt; 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:

Python
# 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 == 403

Other 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 audit and 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
Python
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)
Output
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?

2. What prevents SQL injection in the login example?

3. A customer changes an order ID in the URL and sees another customer's order. Which risk is this?

4. Which is a safe and legal way to practise security testing?

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