Security Testing Basics — Thinking Like an Attacker
The previous page, Performance Testing, asked whether correct behaviour stays fast enough under load. This page asks a harder, adversarial question about that same correct behaviour: can it be misused? The code can return the right answer for every honest input and still hand an attacker the keys — a leaked session token, a database dumped through a search box, an admin page reachable by guessing a URL. Functional tests won’t catch any of it, because they aren’t asking.
Security testing is the lens ground for one class of risk: not “does it work for a good actor?” but “what can a malicious actor make it do?” That single reframe is the whole subject. Everything else — the tools, the vulnerability classes, the escalation rules — is downstream of learning to look at your own feature and ask what an adversary sees.
The reframe: from happy path to hostile input
Section titled “The reframe: from happy path to hostile input”Most testing imagines a cooperative user. They type a real name into the name field, a real
URL into the URL field, and click the buttons in the order you designed. Security testing
imagines the opposite user — one who types a ' into the name field to see if it breaks a
SQL query, who edits the ?userId=42 in the URL to 43, who replays your logout request,
who sends 10,000 login attempts a second.
FUNCTIONAL TESTER asks: SECURITY TESTER asks: ───────────────────── ───────────────────── "given valid input, "given HOSTILE input, is the output correct?" what can I make it do?"
name = "Alice" ─► name = "'; DROP TABLE users;--" /orders/42 (mine) ─► /orders/43 (someone else's) log in as me ─► log in as someone else one request ─► a million requestsThe mental shift is to stop trusting the boundary between your code and the outside world. Every place data crosses in from a client — a form field, a URL parameter, a header, an uploaded file, a JSON body, a cookie — is a place an attacker controls the bytes. Security testing is the discipline of walking those crossing points and asking, at each one, what’s the worst thing these bytes could do?
A useful habit: for every input, name the trust boundary it crosses and the thing of value on the other side. The value is what the attacker is after — user data, money, access, compute. If an input can reach something valuable without being checked, you’ve found a test to write.
The tool families: SAST, DAST, and dependency scanning
Section titled “The tool families: SAST, DAST, and dependency scanning”You do not do this all by hand. Three families of automated tools each look for a different shape of problem, from three different angles. They overlap little and complement well — run all three, because each is blind to what the others catch.
┌─────────────┬──────────────────────┬─────────────────────────┐ │ family │ looks at │ catches │ ├─────────────┼──────────────────────┼─────────────────────────┤ │ SAST │ your SOURCE CODE │ dangerous patterns: │ │ (static) │ (not running) │ tainted input → sink, │ │ │ │ hardcoded secrets │ ├─────────────┼──────────────────────┼─────────────────────────┤ │ DAST │ the RUNNING APP │ real exploitable holes: │ │ (dynamic) │ (black-box probing) │ reflected XSS, open │ │ │ │ redirects, missing auth │ ├─────────────┼──────────────────────┼─────────────────────────┤ │ SCA / dep │ your DEPENDENCIES │ known CVEs in the │ │ scanning │ (the libraries you │ libraries and images │ │ │ didn't write) │ you pulled in │ └─────────────┴──────────────────────┴─────────────────────────┘SAST — static analysis of the code you wrote
Section titled “SAST — static analysis of the code you wrote”SAST (Static Application Security Testing) reads your source without running it, tracing how untrusted data flows. Its core trick is taint analysis: mark data from an outside source (a request parameter) as tainted, then follow it. If tainted data reaches a dangerous sink — a SQL string, a shell command, an HTML page — without passing through a sanitiser, that’s a finding. SAST also flags hardcoded secrets, weak crypto calls, and known-unsafe functions. It runs early (right in the editor or on every commit) and sees every path, even ones no test exercises. Its weakness is false positives: it can flag a “tainted → sink” flow that is actually safe because of a check it couldn’t reason about.
DAST — probing the app while it runs
Section titled “DAST — probing the app while it runs”DAST (Dynamic Application Security Testing) treats the running application as a black box and
attacks it, exactly as an external attacker would — no source code needed. It crawls the app,
then fires malicious inputs at every field and endpoint and watches the responses: did a
script it injected come back un-escaped (XSS)? Did a ../../etc/passwd in a path return a
system file? Did an endpoint that should require login answer without one? DAST finds real,
reachable vulnerabilities — no false-positive guessing about whether a flow is exploitable,
because it just exploited it. Its weakness is the mirror of SAST’s: it only tests what it can
reach, so a bug behind an un-crawled page or a rare state goes unseen.
Dependency / SCA scanning — the code you didn’t write
Section titled “Dependency / SCA scanning — the code you didn’t write”Most of your application, by line count, is code you never wrote: frameworks, libraries, base container images. When a vulnerability is discovered in one of them, it gets a public CVE identifier — and now anyone can look up exactly which versions are exploitable. Software Composition Analysis (SCA) scanners read your lockfiles and images, list every dependency and its exact version, and cross-reference a CVE database to tell you which of your dependencies are known-vulnerable and what to upgrade to.
This is the same job the DevOps book hands to Trivy in the pipeline — scanning both the dependency manifest and the built container image for known CVEs on every build, and failing the build on anything above a severity threshold. Dependency scanning is the highest-value, lowest-effort security testing you can adopt: it is fully automated, it runs in CI, and it catches the class of bug (a known hole in a popular library) that attackers scan the internet for en masse.
The common vulnerability classes to test for
Section titled “The common vulnerability classes to test for”Automated tools are a floor, not a ceiling. To test well you need to know the shapes of the common bugs, because these are the questions you’ll ask by hand on any high-risk feature. They map closely to long-standing industry lists (the OWASP Top 10 is the best-known; consult its current edition for the authoritative, up-to-date ranking).
Injection — untrusted input interpreted as code
Section titled “Injection — untrusted input interpreted as code”The oldest and deadliest class. It happens whenever input is concatenated into a command that some interpreter will execute — SQL, a shell, an LDAP query, HTML. The fix is always the same shape: never mix data and code by string-building; use a mechanism that keeps them separate.
VULNERABLE — input becomes part of the query: query = "SELECT * FROM users WHERE name = '" + input + "'" input = "' OR '1'='1" ─► returns every row input = "'; DROP TABLE users;--" ─► deletes the table
SAFE — input is a bound parameter, never code: query = "SELECT * FROM users WHERE name = ?" (value bound separately) input = "' OR '1'='1" ─► looks up a user literally named that stringTo test for it: feed every input a payload that would break out of the surrounding syntax —
a quote, a semicolon, a <script> tag — and confirm it is treated as inert data, not
executed. If the app misbehaves, the boundary between data and code leaked.
Broken authentication — proving who you are
Section titled “Broken authentication — proving who you are”Authentication answers who are you? It breaks when identity can be faked, stolen, or brute-forced: passwords with no rate limit, session tokens that never expire or are guessable, tokens sent over plain HTTP, “remember me” cookies that don’t rotate on logout. To test: try to log in as someone else without their password. Replay a captured session token after logout — does it still work? Hammer the login endpoint — does anything slow you down? Change your password — do old sessions die?
Broken access control — proving you’re allowed
Section titled “Broken access control — proving you’re allowed”Distinct from authentication: authorization answers are you allowed to do this? The classic bug is IDOR (Insecure Direct Object Reference) — the app checks that you’re logged in but not that the thing you asked for is yours.
You are user 42, logged in. You request: GET /api/orders/42 ─► 200, your order (correct) GET /api/orders/43 ─► 200, someone else's! (broken access control) ^ auth checked you're logged in, never checked order 43 is YOURSTo test: log in as a low-privilege user and try to reach another user’s data or an admin-only action by editing the ID, the URL, or the request. Access control must be checked on the server, for every request — hiding a button in the UI is not a control.
Untrusted input handling — the root of most of the above
Section titled “Untrusted input handling — the root of most of the above”Zoom out and most of these classes share one root cause: input from outside was trusted
somewhere it shouldn’t have been. Injection is trusting input as code; XSS is trusting
input as HTML; a path-traversal (../../etc/passwd) is trusting input as a filesystem path.
The universal defence is to validate at the boundary (reject what isn’t a well-formed,
expected value) and encode at the sink (escape data for the specific interpreter — SQL,
HTML, shell — right before it’s used). Security testing pressure-tests both: send input that
is malformed, oversized, wrong-typed, or laced with metacharacters, and confirm the app
refuses it or neutralises it rather than acting on it.
Baseline, not audit: know the boundary of your job
Section titled “Baseline, not audit: know the boundary of your job”Everything above is the developer-owned security baseline — the testing you can and should do yourself, on every change, mostly automated. It is emphatically not a full security audit or penetration test, and treating it as one is dangerous. Automated scanners find known classes of known bugs; they cannot reason about your business logic, chain three small flaws into one exploit, or think laterally the way a skilled human attacker does. A clean SAST/DAST run means “no obvious holes of the known shapes,” not “secure.”
So draw the line honestly: your baseline buys down the common, automatable risk that catches lazy attackers and old CVEs. A professional pen-tester or security review buys down the sophisticated, logic-level risk that automation structurally can’t see. They are different tools for different tiers of threat — the same lesson as every other page in this part, applied to expertise instead of test type.
When to escalate beyond the baseline
Section titled “When to escalate beyond the baseline”Some surfaces carry consequences severe enough that automated scans alone are negligent. Reach for a dedicated human review — a pen-test, a threat model, a security-focused design review — when a change touches any of these high-blast-radius surfaces:
- Authentication and session management — the front door. A flaw here compromises everyone.
- Payments and money movement — direct financial loss, plus a compliance regime (PCI DSS) that mandates review.
- Personal / sensitive data (PII, health, financial) — a breach carries legal exposure (GDPR, HIPAA-style law) that dwarfs the engineering cost.
- Anything that grants privilege or trust — role changes, admin actions, token issuance, file uploads that get executed or served.
- A new trust boundary — a first-time integration, a new public endpoint, a new way for outside data to enter.
The rule of thumb: the higher the value on the other side of the boundary, the further past the automated baseline you should go. Run the scanners on everything; bring in the experts for the surfaces where being wrong is catastrophic.
The architect’s lens
Section titled “The architect’s lens”- Why does it exist? Because correct software can still be misused. Functional and performance tests verify behaviour for cooperative users; security testing verifies what a hostile user can extract or destroy — a failure mode nothing else on the tester’s bench asks about.
- What problem does it solve? It systematically finds the places where untrusted input crosses a trust boundary and reaches something of value unchecked — injection, broken auth, access-control gaps — before an attacker finds them for you.
- What are the trade-offs? Automated tools are cheap but partial: SAST throws false positives, DAST only tests what it reaches, and none of them understand your business logic. The baseline buys the common risk cheaply; the expensive, expert tier is still required for the sophisticated risk.
- When should I avoid it? Never skip the near-free layer (dependency scanning belongs on every build). But you do down-scope it: a copy-only change to static help text crosses no trust boundary and warrants no security testing beyond what already runs in CI.
- What breaks if I remove it? You ship code that passes every functional test and is still
exploitable — the answer is right, but a search box dumps the user table, or
/orders/43returns a stranger’s order. The failures are silent until they’re a breach, and a breach is the most expensive failure in this whole part.
Check your understanding
Section titled “Check your understanding”- State the reframe at the heart of security testing in one sentence, and explain why a change can pass every functional test and still be a serious security failure.
- Name the three tool families (SAST, DAST, SCA/dependency scanning), what each one looks at, and one weakness of each. Why run all three rather than picking one?
- Explain the difference between broken authentication and broken access control, and give a concrete test you’d run for each.
- Injection, XSS, and path traversal are three different vulnerability classes. What single root cause do they share, and what is the two-part universal defence?
- Why is the developer-owned security baseline not a substitute for a pen-test, and name two high-risk surfaces that should trigger escalation to a dedicated review.
Show answers
- The reframe: stop asking “does it work for a good actor?” and start asking “what can a malicious actor make it do?” A change can pass every functional test because functional tests only check that the output is correct for cooperative input — they never send hostile input, so an exploitable hole (a search box that runs injected SQL, a URL that returns someone else’s data) produces correct-looking behaviour on the happy path and is simply never exercised.
- SAST reads your source code (static, not running) and traces tainted input to dangerous sinks; weakness: false positives from flows it can’t fully reason about. DAST probes the running app as a black box and actually exploits holes; weakness: only tests what it can reach/crawl. SCA/dependency scanning reads your dependencies (libraries, images) and flags known CVEs; weakness: only catches known, published vulnerabilities, nothing novel. Run all three because each is blind to what the others catch — different angles, little overlap.
- Authentication answers who are you? — it breaks when identity can be faked, stolen, or
brute-forced (test: replay a session token after logout, or hammer the login endpoint for a
missing rate limit). Access control / authorization answers are you allowed to do this? —
it breaks when a logged-in user can reach data or actions that aren’t theirs (test: as a
low-privilege user, change
/orders/42to/orders/43and see if you get a stranger’s order — the IDOR test). - Shared root cause: untrusted input was trusted somewhere it shouldn’t have been — as code (injection), as HTML (XSS), or as a filesystem path (traversal). The two-part universal defence: validate at the boundary (reject anything that isn’t a well-formed, expected value) and encode/escape at the sink (neutralise data for the specific interpreter — SQL, HTML, shell — right before use), e.g. bound query parameters instead of string-concatenated SQL.
- Because automated scanners only find known shapes of known bugs — they can’t reason about business logic or chain small flaws into an exploit the way a skilled human can; a clean run means “no obvious known holes,” not “secure.” Escalate on high-blast-radius surfaces such as authentication/session management and payments (also: PII/sensitive data, anything granting privilege, or a brand-new trust boundary).