Skip to content
intermediate Phase 14 · Accessibility & Security Testing Basics

OWASP Security Testing Basics

Probe injection and XSS risks, avoid error leakage, and audit dependencies.

1h 10m
0 problems
Topic Progress 0%

Probing a Search Box for SQL Injection With Safe Payloads

A staging search box is the fastest door into the OWASP Top Ten. You type an apostrophe into product search, press Enter, and the page returns 500 Internal Server Error next to a trace that names products.sql line 42. You did nothing clever: the quote terminated the query string the developer built by concatenation. The 2021 revision files this under A03: Injection, alongside NoSQL injection and OS-command injection. SQL injection has kept its place on every Top Ten since 2003 because each new generation of code reintroduces it.

The list is a prioritization tool, not a checklist to walk top to bottom. Sort it by what a single unauthenticated request can reach:

2021 category Probe this first Where you test it
A01 Broken Access Control another account's id in the URL: /api/orders/1039 while owning 1040 object-level authorization
A03 Injection search, sort, filter, and login fields any input that reaches a query or a shell
A05 Security Misconfiguration verbose errors, default credentials, directory listing chapter 3 of this topic
A06 Vulnerable and Outdated Components dependency scan chapter 3 of this topic
A07 Identification and Authentication Failures login abuse and weak recovery flows the next topic

A01 and A03 go first because neither one needs credentials to start.

Probe with a ladder of payloads that can change query logic but never change data. You have written authorization, you operate on your own rows, and you stop at proof:

'                           syntax-error page or 500    the quote reached a query
' OR '1'='1                 full catalogue, not one row tautology accepted
' AND '1'='2                empty result set            boolean logic reaches the DB
' UNION SELECT NULL--       column-count error          union extraction attempted
'; WAITFOR DELAY '0:0:5'--  response about 5s slower    time-based blind (SQL Server)

No DROP, no DELETE, nothing you would regret running against your own account. If 1=1 and 1=2 return different bodies you have boolean-blind proof; if the delay tracks your SLEEP(5) or WAITFOR, it is time-based. Either difference is the evidence a bug report needs.

Do not stop at the visible form. Sort parameters (?sort=name;--), filter values, X-Forwarded-For, and even cookie values frequently reach the same query unfiltered, and they are the inputs nobody remembered to validate.

Some sinks fire later than the request that planted them: a display name saved at signup is interpolated into an admin's report query two days afterward. Store the payload in your own profile, then visit the page that reuses it — that is second-order injection, and it is why input validation alone never closes A03.

Once you have proof, verify the fix instead of trusting the ticket. Parameterization keeps the query and the data in separate channels:

# vulnerable: the value edits the query text
cur.execute("SELECT * FROM products WHERE name = '" + name + "'")

# safe: query text is fixed, the value travels beside it
cur.execute('SELECT * FROM products WHERE name = %s', (name,))

Two checks close the finding: the search service's database account should hold only SELECT privileges so an injected statement still cannot delete, and any WAF rule you add is a filter, never the fix.

For an interview, "How would you test for SQL injection?" expects a procedure rather than a payload list: confirm scope and authorization, probe with non-destructive payloads, point at the boolean or timing delta as evidence, then verify parameterization and least privilege on the fix. sqlmap belongs in that story only as authorized automation after manual proof. The scan you ran in Automated Accessibility Audits checks markup and ARIA, not query logic — different tool, different blind spots. Error leakage and vulnerable packages are the quieter half of this topic, covered next; once you can force a verbose 500 you are ready for Auth and Session Security Checks.

Which XSS Payload Actually Executes: Reflected, Stored, and DOM

You submit <script>alert(1)</script> in a comment field and the page prints it back as literal text. It is tempting to declare the field safe. Most naive filters block exactly that string, so a real test keeps rotating payloads until something executes, and the ones that usually survive are event handlers with no closing tag:

<script>alert(1)</script>            classic, stripped by naive filters
<img src=x onerror=alert(1)>         no closing tag required
<svg/onload=alert(document.domain)>  fires with no user interaction
" autofocus onfocus=alert(1)>        breaks out of a double-quoted attribute
' onmouseover=alert(1) x='           breaks out of a single-quoted attribute

Run every payload against your own account, stop at alert(document.domain) — proof of execution with no data touched — and record exactly where in the page it fired.

A practical order for a comment field: submit a marker (xqz123) first so you can find your input in the HTML, rotate payload variants one at a time, then view the comment as a second account. Check the moderator's queue and the email notification too, because a payload that only executes for the person reviewing content is still stored XSS, and moderators are the highest-value target on the page.

Three flavors are separated by one question: who else sees this payload, and did the server ever receive it?

Flavor Who executes it How you trigger it
Reflected only the person who sent the request crafted link or search term echoed in the response
Stored every viewer of the saved content comment, bio, or ticket title persisted server-side
DOM-based the client alone — the server never saw the input location.hash or a postMessage read into the DOM

DOM-based XSS hides because server-side filters and WAF rules never observe the input. One line shows the shape of the sink:

const term = new URLSearchParams(location.search).get('q');
document.getElementById('results').innerHTML = term;   // sink, no server round trip

textContent fixes it outright; when markup is required, sanitize with a maintained library such as DOMPurify before assignment.

Encoding is contextual, and that is the part interviewers listen for. Enter a marker, then the sequence <>'", and find where the input lands: an HTML body needs entity encoding (&lt;), a double-quoted attribute needs quote encoding, a JavaScript string needs escaping of quotes, backslashes and </, and a URL parameter needs percent-encoding. One global <script> blacklist protects exactly one of those four contexts.

Verify the mitigations instead of assuming them. Fetch the header with curl -sI https://staging.example.com/profile | grep -i content-security-policy and look for a policy without unsafe-inline, then re-send the payload and confirm the console logs a violation instead of executing. A policy installed only on the login page is not a policy — check every response that renders user content. Then read what it actually restricts: a Content-Security-Policy such as default-src 'self'; script-src 'nonce-a1b2c3' blocks inline event handlers even after a payload makes it into the DOM, and an HttpOnly cookie keeps document.cookie from reading the session — although script running in your origin can still act as the user, so output encoding and CSP remain the real defense. One taxonomy note for interviews: XSS was standalone category A7 in 2017 and folded into A03 Injection in the 2021 list.

An XSS that lifts a token from localStorage is exactly the scenario Auth and Session Security Checks is built to close, and it arrives immediately after this topic's dependency audit.

Error Pages and Dependency Audits: What the App Volunteers

Attackers read before they break in. Force an error the cheap way — request /api/products/abc where the id column is an integer, post malformed JSON ({"qty": }), or submit a 4 MB field — and then read what the response gives away. Repeat the probe with a wrong content-type (sending text/plain to a JSON endpoint) and with an oversized field, because each failure path is often handled by different middleware and leaks differently.

The response revealed What it buys an attacker
relation "orders" does not exist raised in /home/deploy/api/src/db/queries.js:84 deploy path, file layout, query layer
header X-Powered-By: Express or Server: Apache/2.4.49 (Ubuntu) exact versions to match against CVE feeds
a Django page with DEBUG=True and a settings dump SECRET_KEY, database URLs, cloud keys if present
a stack trace inside an HTML comment on a 200 page everything above, without even a 500

The production response should be boring:

HTTP/1.1 500 Internal Server Error
content-type: application/json
cache-control: no-store

{"error":"internal_error","referenceId":"7f3c91ab"}

The stack trace, query text, and file paths belong in the server log keyed by that referenceId, so a support ticket correlates to the exception without shipping diagnostics to strangers. Suppressing errors is not the fix: app.disable('x-powered-by') in Express, server.error.include-stacktrace=never in Spring, and DEBUG=False on every non-development deployment are configuration decisions your suite should assert — a response-header check belongs in the API regression pack next to your status-code assertions.

Dependencies are the quieter leak, and the scan is one command that CI should own:

npm audit --audit-level=high      # non-zero exit when high+ found; gates the build
npm audit fix                     # applies semver-compatible bumps only
npm audit fix --force             # may jump majors — never run it blind in CI
npm ls lodash                     # shows which path pulled the vulnerable version

The scan only means something when the tree is pinned: commit package-lock.json and install with npm ci in CI, because a loose npm install can resolve a different transitive version on the agent than on your laptop and let yesterday's finding vanish for the wrong reason. Use npm audit --omit=dev when the report must reflect only what ships.

Each ecosystem has its equivalent: pip-audit -r requirements.txt, mvn org.owasp:dependency-check-maven:check, bundle audit --update on Ruby. They all do the same job — reporting a version number that sits in the tree.

Severity is a queue, not a decision. Triage every finding with three questions: is the package reachable from the code you ship (a CVE reached only through jest's transitive minimist never executes in production), is a fixed version available and does it patch a path you actually call, and can you mitigate now — npm overrides pins a transitive dependency without waiting on its parent:

"overrides": { "minimist": "1.2.8" }

A deferral an auditor accepts in one line reads like this: minimist 1.2.5 to 1.2.8, reachable only through webpack-dev-server, dev-only, no runtime path, owner: platform, sprint 14 — version, reachability, owner, date. Gate the pipeline on runtime high and critical findings, log dev-only ones for a weekly cleanup, and require a ticket carrying the reachability reasoning for anything deferred. That discipline is the same one you built running axe in CI for Automated Accessibility Audits: a scanner earns its keep only when its output has an owner. Next you apply that rigor to credentials and sessions in Auth and Session Security Checks.

Quiz

1. You are authorized to test a product search field on staging. Which payload proves a SQL injection while leaving data untouched?

Question 1 options

2. You save `<img src=x onerror=alert(1)>` in your profile bio. It executes for every visitor who opens your profile, including an admin reviewing the account. How is this classified?

Question 2 options

3. A 500 response body contains `at handler (/home/deploy/app/src/routes/orders.js:84:11)`. What is the correct remediation?

Question 3 options

4. Your pipeline must fail when a high-severity CVE lands in production dependencies but must not block the build on dev-only findings. Which setup does that?

Question 4 options

Flashcards

Question

Which 2021 OWASP Top Ten categories should you probe first, and what makes them first?

Answer

A01 Broken Access Control and A03 Injection, because a single unauthenticated request can reach both — an object id in a URL or an unvalidated search parameter — while categories like A06 need a dependency scan and A07 needs a login flow to exercise.

Question

What makes a SQL injection probe safe to run on a shared staging database?

Answer

The payload changes query logic but never data: boolean pairs like ' AND '1'='1 versus ' AND '1'='2, tautologies that only alter the result set, or time-based payloads such as WAITFOR DELAY. No DROP or DELETE, testing only on your own rows with written authorization, and stopping once the difference in responses proves the hole.

Question

Name the three XSS flavors and the single question that separates them.

Answer

Reflected (payload is echoed in the response to the sender), stored (payload is saved and executes for every viewer), DOM-based (client-side sink such as innerHTML fed from location.hash — the server never receives the input). The separating question: did the server get the input, and who else sees the rendered result?

Question

What should a production error response contain, and where does the detail go?

Answer

A generic machine-readable message plus a correlation ID (for example {"error":"internal_error","referenceId":"7f3c91ab"}), with cache-control: no-store. Stack traces, file paths, and SQL text stay in server logs keyed by that ID; X-Powered-By and Server version headers are removed and DEBUG stays off outside development.

Question

How do you triage an npm audit finding before deciding whether to block a release?

Answer

Ask three questions: is the package reachable from shipped code (a jest-only transitive dep never runs in production), is there a fixed version that patches the path you call, and can you pin it immediately with an npm overrides entry. CI gates on runtime high/critical via npm audit --audit-level=high; dev-only findings go to a tracked backlog ticket.

Revision Notes

Key Takeaways

  • 1. Sort the Top Ten by reachability: A01 and A03 are reachable without credentials, so they lead the test plan; A06 is answered by a scanner, A07 by the auth flows in the next topic
  • 2. Prove injection with payloads that change logic but not data — boolean pairs and timing deltas are evidence, DROP and DELETE are incidents
  • 3. XSS classification hinges on persistence and audience: reflected hits the sender, stored hits every viewer, DOM-based never reaches the server — and encoding is per context, not a global `<script>` blacklist
  • 4. Error responses should be boring (generic message + correlation ID) with stack traces in logs; version-revealing headers and DEBUG flags are assertable API checks
  • 5. Dependency severity is a queue, not a decision: reachability and fix availability decide whether the release blocks, and npm overrides pin transitive packages without a parent upgrade

Interview Tips

  • • "How would you test a form for SQL injection?" — answer as procedure: confirm authorization, ladder of non-destructive payloads, cite the boolean or timing delta as evidence, then verify parameterization and SELECT-only privileges on the fix
  • • "Reflected, stored, or DOM-based — how do you tell?" — pivot on two questions: did the server receive the input, and who else sees the rendered payload; give one trigger example for each
  • • "What do you check in an error page?" — stack traces, file paths, SQL text, framework and server versions in headers, DEBUG flags; the fix is a correlation ID plus server-side logs, not suppressing errors
  • • "How do you handle a high-severity CVE in a transitive dependency?" — reachability first, then fix availability, then mitigation via overrides or an upgrade; explain why npm audit fix --force inside CI is dangerous

Cheat Sheet

OWASP Security Testing Basics

Order the plan by what one unauthenticated request reaches:

Category (2021) Probe Tool / evidence
A01 Broken Access Control swap object ids in URLs, change X-User-Id headers second account's data returned
A03 Injection search/sort/filter/login inputs boolean or timing delta, alert(document.domain)
A05 Misconfiguration force errors, check headers, default creds stack traces, X-Powered-By, DEBUG=True
A06 Vulnerable Components dependency audit npm audit --audit-level=high
A07 Auth Failures login abuse, recovery flows next topic

Safe SQLi ladder (authorized targets only):

'                 -> error page means the quote reached a query
' OR '1'='1       -> full result set: tautology accepted
' AND '1'='2      -> empty set: boolean logic evaluated by the DB
'; WAITFOR DELAY '0:0:5'--  -> ~5s: time-based blind

XSS by context — encode for where the input lands: HTML body → entity encode; attribute → quote encode; JS string → escape quotes/backslashes/</; URL → percent-encode. Sinks: innerHTML, document.write, javascript: hrefs. Mitigations to verify: CSP without unsafe-inline, HttpOnly cookies, DOMPurify for markup.

Error hygiene checks:

curl -i https://staging.example.com/api/products/abc   # expect generic 4xx/5xx + referenceId
curl -i https://staging.example.com/ | grep -iE 'x-powered-by|server:'   # expect nothing versioned

Dependency gate: npm audit --audit-level=high (exit code gates CI), npm audit fix (semver-safe only), npm ls <pkg> (find the path), npm overrides (pin transitive now). Mirrors: pip-audit, dependency-check-maven, bundle audit.

Continue Learning