Security for Developers

Security in Practice: Code Review Checklist, Testing, Incidents and Interview Answers

A review checklist you can use tomorrow, SAST, DAST and dependency scans, what to do in the first hour of an incident, and the security questions interviewers ask with model answers.

Lesson 0816 min read#security#code-review#incident-response#interview

On this page

  1. Words used in this lesson
  2. A code review checklist
  3. The four kinds of scanners
  4. A workflow that runs the cheap checks
  5. Threat modelling in ten minutes
  6. The first hour of an incident
  7. Security champion habits
  8. Try it
  9. Interview questions
  10. Key takeaways
  11. What's next

In one line: security is a habit, not a project. Put a short checklist into every code review, let cheap tools catch the boring mistakes, know your first-hour moves for an incident before it happens, and be the person on the team who asks "what could a stranger do with this?"

Lesson 0 gave you the mindset and the OWASP map. Lesson 1 covered injection. Lesson 2 covered XSS, CSRF and headers. Lesson 3 covered passwords and login. Lesson 4 covered access control. Lesson 5 covered secrets, encryption and data. Lesson 6 covered the supply chain. Lesson 7 covered APIs and servers. This last lesson ties all of that into things you can use tomorrow: a review checklist, the four kinds of scanning tools (and what each can never find), a workflow file that runs them, an incident checklist, some team habits, and a long set of interview questions with model answers.

For the lead's view, read Team Lead Lesson 14 (A Security Mindset for Team Leads) and Team Lead Lesson 4 (Incidents, Team Health and Growing as a Lead). For CI/CD that deploys see DevOps Lesson 14. For writing the tests that guard these fixes see Testing Lesson 3. For the code review habit see Git and GitHub Lesson 3.

What I ran / checked. I ran these on Node v22.22.0, npm 10.9.4, gitleaks 8.30.1 (the Linux binary from the project's release page) and Semgrep 1.180.0 (installed with pip into a virtual environment), in scratch folders outside this repository, on toy code I wrote. Run for real: (1) gitleaks against a tiny Git repository that held a made-up random token shaped like a GitHub token, in a commit, then after I deleted the file in a later commit; (2) Semgrep with two small rules I wrote, against a toy Express file that has an SQL injection, a command injection, a safe query and an IDOR route; (3) npm audit on a deliberately old lodash@4.17.4 (the output is in Lesson 6); (4) git ls-remote to get the real commit hashes of the actions used in the workflow; (5) the workflow YAML below was parsed with PyYAML 6.0.1 and loaded without errors. Not run: the GitHub Actions workflow itself, CodeQL (it needs GitHub's CLI and database), OWASP ZAP (I tried to use Docker, but its daemon was not running in my sandbox), and anything that needs a live GitHub repository. Sources I read: the OWASP Cheat Sheet Series pages for Threat Modeling, CI/CD Security, Logging, Error Handling, Software Supply Chain Security and Vulnerable Dependency Management (the last one has a section on handling a vulnerability report and patches); the gitleaks README (the git, dir and stdin scan modes) and the gitleaks-action README (an example workflow, and the note that organisations need a licence key); the Semgrep README (it says the community edition analyses within a single function or file); the github/codeql-action README; the GitHub "Secure use reference" for Actions; the Dependabot options reference; the OWASP Top 10:2025 introduction; and the Node.js security guide. Search-result summaries only (not full pages): the OWASP API Top 10 (2023) list, and the incident facts mentioned (tj-actions/changed-files, Shai-Hulud). I did not read the OWASP ZAP documentation in detail, so what I say about ZAP is general knowledge.

Words used in this lesson

  • Code review is when another person reads your change before it is merged.
  • SAST (static application security testing) reads your source code, without running it, and looks for dangerous patterns.
  • DAST (dynamic application security testing) attacks a running app from the outside, like a very patient stranger, and looks at the responses.
  • SCA (software composition analysis) checks the packages you use against a list of known vulnerabilities. npm audit and Dependabot do this.
  • Secret scanning looks for keys, tokens and passwords in code and Git history.
  • False positive is an alarm for something that is not a problem. False negative is a real problem the tool did not see.
  • Triage is the first sorting: is this alert real, how bad, who fixes it, how soon.
  • Incident is an event that harms, or may harm, the confidentiality, integrity or availability of your system or data.
  • Contain means stop the damage from growing (cut access, take a service offline). Rotate means replace a secret with a new one and kill the old one.
  • Post-mortem is a written review after an incident: what happened, why, and what we change. A blameless one asks "what in our system let this happen?" and not "who did it?".
  • Threat model is a short, structured answer to "what are we building, what can go wrong, what do we do about it?"
  • Defence in depth means several layers of protection, so one failure is not the end.
  • Security champion is a developer on a team who takes extra interest in security and helps the others, without being a full-time security person.
  • Shift left means finding problems earlier in the process, for example at code review instead of in production.

A code review checklist

Most security bugs do not need a special tool to spot. They need a reviewer who asks the right question at the right line. Use this table in pull requests. The "smell" column is something you can actually see in a diff.

What to look forLessonA smell in the diff
User input goes into a query or command1"SELECT ... WHERE id = " + id, `... ${name}` in SQL, exec("ls " + dir), eval, new Function
User input goes into HTML2innerHTML = ..., dangerouslySetInnerHTML, templates with escaping turned off, v-html
A state-changing request with cookies and no CSRF defence2A POST/DELETE route that relies only on a cookie; cookies without SameSite
Security headers and cookie flags2A new response path without CSP; cookies missing HttpOnly, Secure, SameSite
Password handling3md5, sha1, plain sha256 for passwords; no salt; passwords in logs
Login and reset flows3No rate limit; "no such user" vs "wrong password"; reset tokens that do not expire or can be reused
Session and token use3JWT with alg: none allowed, no exp, secret in the code; token in localStorage for a sensitive app
Authorisation on every route4findById(req.params.id) with no owner check; a role check only in the UI; deny-by-default missing
Mass assignment4User.update(req.body) or {...req.body} into a model
Secrets in code or config5A key-like string in the diff; .env committed; a secret in front-end code or a build argument
Crypto used the right way5Math.random() for tokens; ECB mode; home-made encryption; a fixed IV; HTTP URLs for APIs
Personal data5New fields with names, emails, health data; data in logs; no retention plan
New or changed dependency6A new package with few users; a lock file change with an odd resolved host; postinstall in its package.json
CI and workflow changes6uses: some/action@main; pull_request_target with a checkout of PR code; ${{ github.event... }} inside run:; broad permissions
Server fetches a URL7fetch(req.query.url), axios.get(userUrl), an image or webhook feature with no host check
File upload7path.join(dir, file.name), trust in Content-Type, no size limit, saved under the web root
No limits7A new expensive endpoint with no rate limit; ?limit= without a maximum; unbounded loops over user input
Errors and logs7res.send(err.stack); res.json(err); console.log(req.body) on login routes
CORS7Access-Control-Allow-Origin set from req.headers.origin; credentials: true with * or a pattern
Failing open7A catch that lets the request through; "if the auth service is down, allow"
Is there a test?Testing 3A security fix with no test that would fail if the fix were removed

Three habits make the checklist work:

  1. Review the diff for new "doors". A new route, a new input, a new file read, a new outbound call, a new permission, a new package. The bug is nearly always at a door, not in the middle of a room.
  2. Ask the threat-model questions out loud. "What if this ID belongs to someone else?" "What if this string is ../../etc/passwd?" "What if this request is sent 10,000 times?"
  3. Do not review only the green lines. A security fix often removes a line (a check). If a PR deletes a check, ask why.

The four kinds of scanners

New developers hear "run a security scan" and think of one tool. In truth there are four kinds, and each sees something different. Each also has blind spots, and the blind spots are the part to remember.

KindLooks atOne free toolFinds wellCannot find
SASTYour source code (not running)Semgrep (Community Edition), or CodeQL (free for public repos on GitHub)Dangerous patterns: SQL built with +, eval, hard-coded keys, weak crypto callsMissing checks that leave no pattern (an IDOR, a missing role check); problems that only exist at run time or in configuration; business-logic flaws
DASTA running app, from outsideOWASP ZAPMissing headers, reflected XSS, some injection, exposed paths, bad cookie flags, verbose errorsCode that no request reaches; things behind a login the tool cannot do; logic bugs; it tells you what is wrong but not which line
SCAYour dependencies and their versionsnpm audit (also Dependabot)Known CVEs in packages you use, including transitive onesA brand new malicious version nobody reported; bugs in your own code; whether your code reaches the vulnerable function
Secret scanningCode and Git historygitleaks (also GitHub secret scanning)Tokens, keys and passwords with a known shapeSecrets with no clear shape (a random password in a string); secrets that live outside Git (a chat message, a ticket); the fact that a leaked secret was already copied

None of them finds a design flaw ("anyone with the link can see the invoice") and none replaces a person thinking. They are good at the boring 80 percent, so humans can spend their time on the other 20.

SAST, for real: Semgrep on a toy file

I wrote a small Express file with four routes: a user lookup that builds SQL with +, a command runner that builds a shell command with +, a safe query with a placeholder, and an orders route that has an IDOR (no owner check):

JavaScript
app.get("/user", (req, res) => {  const id = req.query.id;  // BAD: user input is glued into the SQL text  db.query("SELECT * FROM users WHERE id = " + id, (err, rows) => res.json(rows));});app.get("/run", (req, res) => {  // BAD: user input goes to a shell  require("child_process").exec("ls " + req.query.dir, (e, out) => res.send(out));});app.get("/safe", (req, res) => {  db.query("SELECT * FROM users WHERE id = ?", [req.query.id], (err, rows) => res.json(rows));});app.get("/orders/:id", (req, res) => {  // BAD (IDOR): no check that this order belongs to the logged-in user  db.query("SELECT * FROM orders WHERE id = ?", [req.params.id], (err, rows) => res.json(rows));});

And two rules in Semgrep's YAML format. A rule is a code pattern with holes: $X matches any expression and ... matches anything. The rules say "a .query() call whose first argument is a string plus something" and the same for .exec():

YAML
rules:  - id: sql-string-concat    languages: [javascript]    severity: ERROR    message: SQL text is built with + from a variable. Use placeholders (parameterised query).    pattern: $DB.query("..." + $X, ...)  - id: exec-with-concat    languages: [javascript]    severity: ERROR    message: A shell command is built from a variable. Use execFile with an argument list.    pattern: $CP.exec("..." + $X, ...)
Shell
semgrep scan --metrics=off --config rules.yml .

This is the real output (the ASCII box lines trimmed a little):

TEXT
┌─────────────────┐│ 2 Code Findings │└─────────────────┘    app.js   ❯❯❱ sql-string-concat          SQL text is built with + from a variable. Use placeholders (parameterised query).            7┆ db.query("SELECT * FROM users WHERE id = " + id, (err, rows) => res.json(rows));   ❯❯❱ exec-with-concat          A shell command is built from a variable. Use execFile with an argument list.           12┆ require("child_process").exec("ls " + req.query.dir, (e, out) => res.send(out)); • Findings: 2 (2 blocking) • Rules run: 2 • Targets scanned: 1Ran 2 rules on 1 file: 2 findings.

Two findings, with the file and line, and the safe query on line 16 is not reported. That is what a good SAST rule gives you. Now look at what is missing. I added the /orders/:id route (the IDOR) after the first run and ran the scan again: still Findings: 2. The IDOR has no bad pattern. Every line looks fine. The absence of an ownership check is invisible to a pattern matcher. Only a reviewer, a test, or a DAST run with two different users can find it. (Semgrep's own README says the Community Edition can analyse only within the boundaries of a single function or file, and that the paid product goes further. I used only the free one and my own two rules, which are small. Real rule packs have thousands of rules.)

A clean scan is not a clean bill of health. A scanner answers "did I see the patterns I know?", not "is this safe?". Tell the team what a tool does not check, so nobody thinks the box is ticked.

Secret scanning, for real: gitleaks

I made a Git repository with two commits. In the first, a file with a fake token: I generated 36 random letters and digits and put ghp_ in front, so it has the shape of a GitHub personal access token but is not one. In the second commit I deleted the file. Then I ran gitleaks:

Shell
gitleaks git -v --no-banner --redact .
TEXT
Finding:     const ghToken = "REDACTED"; // random, fake, ...Secret:      REDACTEDRuleID:      github-patEntropy:     4.784184File:        token.jsLine:        1Commit:      bceb7c262154b3b3066bc48011e72386042cf753Author:      tEmail:       a@b.cDate:        2026-10-11T18:08:14ZFingerprint: bceb7c262154b3b3066bc48011e72386042cf753:token.js:github-pat:1INF 2 commits scanned.WRN leaks found: 1

The exit code was 1, so a CI job fails. (--redact hides the secret in the output, which is a good habit for CI logs. -v shows the findings.) Now the important part. After I deleted the file in a new commit, I scanned two ways:

TEXT
$ gitleaks dir --no-banner --redact .        # the files as they are nowINF no leaks found$ gitleaks git --no-banner --redact .        # the whole Git historyINF 2 commits scanned.WRN leaks found: 1

The current files are clean, but the history still has the secret. Anyone who clones the repository can run git log -p and read it. That is why Lesson 6 says: rotate first. Deleting the file changes nothing about who already has the key. The gitleaks README lists the three modes I used: git (scans commits with git log -p), dir (scans files) and stdin.

A small trap I found: my first test used the well-known AWS example key AKIAIOSFODNN7EXAMPLE from the AWS documentation, and gitleaks said "no leaks found". Tools skip well-known example values to reduce noise. That is good for you, but remember it when you write a test for your own pipeline: use a random value with the right shape.

SCA and DAST

For SCA, Lesson 6 has the real npm audit output for an old lodash. In CI, npm audit --audit-level=high exits with a non-zero code when it finds something at that level or above, which fails the job.

For DAST, the best-known free tool is OWASP ZAP. It acts as a proxy and a scanner: you point it at a running copy of your app (a test environment, never a production system you do not own), and it crawls pages and sends many modified requests, then reports findings with the request and response as proof. I could not run it here (see the note). Use it against your own staging site, with permission, and with test data. A good first step is its "baseline" scan, which is a passive check that does not attack. Remember what it cannot do: it cannot know that user A should not see user B's order unless you give it two logins and a rule.

A workflow that runs the cheap checks

Here is a GitHub Actions workflow with three small jobs: a dependency audit (SCA), a secret scan (gitleaks), and CodeQL (SAST). It uses the safe habits from Lesson 6: actions pinned to commit hashes (I took the hashes from git ls-remote on the day I wrote this), a contents: read token by default, and persist-credentials: false. I checked that it is valid YAML. I could not run it, because it needs GitHub.

YAML
name: Security checkson:  pull_request:    branches: [main]  push:    branches: [main]  schedule:    - cron: "17 3 * * 1"   # every Monday, 03:17 UTCpermissions:  contents: readjobs:  audit:    name: Dependency audit    runs-on: ubuntu-latest    timeout-minutes: 10    steps:      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1        with:          persist-credentials: false      - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0        with:          node-version: 22          cache: npm      - run: npm ci --ignore-scripts      - run: npm audit --audit-level=high  secrets:    name: Secret scan    runs-on: ubuntu-latest    timeout-minutes: 10    steps:      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1        with:          fetch-depth: 0          # the scan needs the full history          persist-credentials: false      - uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0        env:          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}          GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}  # only for organisation repos  codeql:    name: CodeQL    runs-on: ubuntu-latest    timeout-minutes: 20    permissions:      contents: read      security-events: write    # lets CodeQL upload its results    steps:      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1        with:          persist-credentials: false      - uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3        with:          languages: javascript-typescript      - uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
PartWhy
on: scheduleNew advisories appear every day. A weekly run finds a new problem in code that did not change.
top-level permissions: contents: readThe default for every job. Only the codeql job asks for more, and only what it needs.
npm ci --ignore-scriptsInstall exactly the lock file and run no install scripts.
npm audit --audit-level=highFail only on high and critical. Low findings should not stop every PR, or people will turn the check off.
fetch-depth: 0By default checkout fetches one commit. A secret scan needs the history.
GITLEAKS_LICENSEThe gitleaks-action README says organisation accounts need a (free) licence key and personal accounts do not. If you do not want an action, you can run the gitleaks binary in a run: step instead.
languages: javascript-typescriptCodeQL's name for JS and TS code.
security-events: writeCodeQL uploads results to the repository's Security tab.

Do not add scanners that nobody reads. A job that is red for three months teaches the team to ignore red. Start with fewer checks that you fix when they fail. Tune the noisy ones (ignore lists with a comment saying why), and mark the useful ones as required checks in branch protection (Git and GitHub Lesson 5).

Where each tool fits in time

WhenToolWhy then
Before you commitgitleaks as a pre-commit hookA secret that never reaches Git never needs history cleaning
On every pull requestSAST, SCA, secret scan, tests, a human reviewerCheapest moment to fix
Weekly or nightlySCA again, DAST against stagingNew advisories; slower scans
After deployLogs, alerts, uptime checksSome problems only show with real traffic
Once or twice a yearA penetration test by outside peopleFresh eyes, design-level flaws

Threat modelling in ten minutes

You met this in Lesson 0. The OWASP Threat Modeling cheat sheet (quoting the Threat Modeling Manifesto) says the work answers four questions:

  1. What are we working on? Draw it: users, your app, the database, the third parties, and the arrows between them. Mark where data crosses a trust boundary (from the internet to your server, from your server to a partner).
  2. What can go wrong? Walk each arrow and box. A memory aid is STRIDE: Spoofing (pretending to be someone), Tampering (changing data), Repudiation (denying an action), Information disclosure (leaking), Denial of service (making it unavailable), Elevation of privilege (gaining rights).
  3. What are we going to do about it? For each threat: fix it, reduce it, accept it (and write that down), or avoid the feature.
  4. Did we do a good enough job? Check that each fix exists and has a test, and review again when the design changes.

A tiny example for "add a profile picture by URL": the arrow from the user to the server carries a URL (threat: SSRF, Lesson 7); the file we store (threat: a script in an SVG, Lesson 7); who can see it (threat: access control, Lesson 4); how many can be uploaded (threat: disk filling, rate limit). Five minutes, a whiteboard, and you already have four tickets before any code is written.

The first hour of an incident

Something bad has happened: a key is public, an account is hijacked, a server is acting strangely, a package you use was reported as malicious. This is the moment when people panic and make it worse. A short written plan helps, so write it before you need it. This is a simple version for a small team. For the people side (staying calm, roles, blameless reviews), read Team Lead Lesson 4.

MinutesStepWhat to doMistakes to avoid
0 to 5Declare and assignSay "this is an incident". Name one incident lead (decides) and one note-taker. Open one chat room or call.Five people each doing their own fix and not talking
5 to 15ContainStop the damage from growing: disable the account, revoke the token, turn off the feature, block the IP, take the server off the network, roll back the deploy.Deleting things (you lose evidence). Fixing the root cause first while the leak keeps running
15 to 25Rotate secretsReplace every secret that might be exposed: API keys, database passwords, signing keys, tokens, session secrets. Revoke the old ones at the provider. Do the most powerful first.Rotating only the one you know about. Putting the new secret in the same chat, or in Git
25 to 35Preserve evidenceSave logs, copy affected disks or snapshots, keep the request IDs and error IDs, note times (with time zone). Make sure logs are not overwritten by rotation.Restarting the server and losing in-memory state or rotating logs
35 to 45Scope: what did they touch?Use logs to find: first bad event, accounts and data affected, other places the same key or hole was used.Assuming "only one account". Stopping at the first finding
45 to 55Tell peopleTell your manager and the right owners now. Decide about users, customers, partners, and in some cases a regulator. Laws such as GDPR can require telling an authority within a fixed time (72 hours) for personal-data breaches; ask legal or a lead, and do not guess. Use plain words and facts.Hiding it. Guessing about the cause in public. Saying "no data was accessed" before you know
55 to 60Plan the fixWrite what the real fix is, who does it and when. Patch the hole itself (not only the symptom). Add a test.A quick patch that stays forever
Next daysPost-mortemWrite a blameless review: timeline, impact, root cause, what worked, what did not, and actions with owners and dates. Share it.Naming and shaming. A list of actions nobody owns

Notes:

  • Logs are your memory. You can only investigate what you recorded. OWASP's Logging cheat sheet says to log the events that matter for security (failed logins, access-control failures, input validation failures, admin actions) with time, user and request ID, and not to log passwords, tokens or keys. If you have no logs, add them now so the next incident is easier. OWASP's A09:2025 is named "Security Logging & Alerting Failures" on purpose: logs without alerts that someone reads have little value.
  • A leaked key is the most common incident. The recipe is in Lesson 6 and Lesson 5: rotate, check provider logs for use, remove from code, add scanning. For a token stolen from a developer's machine (as in the Shai-Hulud worm), rotate everything that machine could reach.
  • A bad package. If a dependency was reported as malicious: find which versions you installed (the lock file tells you), treat any machine or CI run that installed the bad version as exposed, rotate the secrets they had, pin to a clean version, and check the registry's and the maintainers' notices. The ua-parser-js advice at the time was to treat machines that installed the bad versions as fully compromised.
  • Practise. Once a year, run a one-hour pretend incident ("a key was found on GitHub") and see who knows what to do. You will find gaps, such as nobody knowing how to rotate the database password.

Security champion habits

A security champion is not a security expert and not a gatekeeper. It is a developer who cares a bit more, and makes it easy for the team to do the right thing. You can start now, whatever your job title. Small habits:

  1. Ask one security question in every design talk and PR. "What if this input is hostile?" Asking is the habit.
  2. Keep a one-page team checklist (the table above), in the PR template, short enough to be used.
  3. Own the dependency and alert routine. One day a week, look at the Dependabot PRs and the audit. Write down what you decided.
  4. Make the safe way the easy way. A shared function that does the upload checks, a middleware for auth and rate limits, a helper that makes error IDs. People copy what is easy.
  5. Write the test with the fix. A security bug fixed without a test comes back.
  6. Read one incident report a month. Real post-mortems (public ones from big companies) teach more than slides.
  7. Learn the tools of an attacker, safely. Try the exercises on practice sites (like PortSwigger's Web Security Academy) and only on systems you own or have written permission to test.
  8. Stay kind. When someone finds a hole in your code, thank them. A team that punishes the finder stops finding holes.
  9. Report, don't hide. If you made a mistake (pushed a key), say so fast. Fast reports turn into a 10-minute fix, slow ones into an incident.
  10. Keep learning in small bites. The OWASP Top 10, the OWASP Cheat Sheet Series (short pages on one topic each) and the Node.js security guide are free and good.

Only test what you own or have permission to test. Scanning someone else's website, even "just to see", can be illegal and can break things. Use your own toy apps, a local copy, or sites that invite testing.

Try it

  1. Take a pull request from your own project (or an open source one). Go through the review table and find the first three "doors" in it. Which row of the table applies?
  2. Install gitleaks. Make a new Git repository, add a file with a random ghp_ plus 36 letters or digits, and commit. Run gitleaks git. Delete the file, commit, and run gitleaks dir and gitleaks git again. What is different?
  3. Write a Semgrep rule that finds eval(...) of a non-constant argument in JavaScript. Try it on a file with a safe and an unsafe call.
  4. Copy the workflow above into a practice repository. What do you need to create for the secret-scan job if the repository belongs to an organisation?
  5. Draw a data-flow diagram for a feature you know (login, checkout, comments). Mark every trust boundary. List two STRIDE threats for each.
  6. Write a one-page "key leaked" runbook for your own project: who is the lead, where each secret lives and how to rotate it.
  7. Run a 20-minute pretend incident with a friend: "our database password is in a public repo". Time how long until the password is rotated.
  8. Pick any five questions from the interview section below. Answer them out loud without reading, then compare.

Answers

  1. Typical doors: a new route, a new field, a new package, a changed workflow. Match each to a row: input to query (1), route without owner check (4), package.json change (6).
  2. gitleaks dir reports no leaks (the file is gone), but gitleaks git still reports one, because the history has it.
  3. Something like pattern: eval($X) plus pattern-not: eval("..."). Check that the constant call is not reported.
  4. A GITLEAKS_LICENSE repository secret. The README says organisations need a licence key (free to get) and personal accounts do not.
  5. Trust boundaries are where data crosses from outside to inside, such as browser to server, server to database, and server to a third party. Any threat that fits one of the six STRIDE letters is fine.
  6. A good runbook has names, links, and the exact steps. The test is that someone who has never done it can follow it.
  7. The first time is slow, usually because nobody knows who can change the database password. That is the point of practising.
  8. Speaking aloud finds gaps that reading hides.

Interview questions

These are the security questions I would expect for a developer role. The answers are in first person and short enough to say out loud.

What is the difference between SQL injection and XSS? (Must know)

Both are injection: untrusted data is treated as code. In SQL injection the data ends up in a database query on the server, so the attacker can read or change data. In XSS the data ends up in a web page that runs in another user's browser, so the attacker's script runs as that user. The fix for the first is parameterised queries, so the data stays data. The fix for the second is output encoding for the right place (HTML, attribute, URL, JS), plus a Content Security Policy as a second layer. Both are in the OWASP category Injection (A05:2025). See Lesson 1 and Lesson 2.

What is the difference between 401 and 403? (Must know)

401 Unauthorized really means "I don't know who you are": there are no valid credentials. The user can try again with a login. 403 Forbidden means "I know who you are, and you are not allowed to do this". Logging in again will not help. A related choice: for a resource a user must not even know exists, some APIs return 404 instead of 403, so they do not confirm that it exists. Whichever I choose, the check must happen on the server for every request (Lesson 4).

How should you store passwords? (Must know)

Never in plain text, and never encrypted in a way I can decrypt. I store a slow, salted hash made by a password hashing function: Argon2id, scrypt or bcrypt. The salt is random and unique per user, and stored with the hash, so equal passwords give different hashes and pre-computed tables don't work. Slow means that guessing billions of passwords is expensive. I never use plain MD5, SHA-1 or SHA-256 for this, because they are made to be fast. I also rate-limit login and support MFA (Lesson 3).

How do you stop CSRF? (Must know)

CSRF works because the browser sends the user's cookie on a request that another site triggered. So I make sure the request must carry something that a foreign site cannot make. I use SameSite=Lax or Strict on session cookies, and CSRF tokens (a secret value, checked on every state-changing request), or checking the Origin header or Sec-Fetch-Site. I never change state with a GET. I also remember that CORS does not stop CSRF: it controls reading responses, not sending requests (Lesson 2 and Lesson 7).

What is CORS and what are common mistakes? (Must know)

It is a way for a server to tell the browser which other origins may read its responses. It relaxes the same-origin policy, and only the browser enforces it. Common mistakes: reflecting the Origin header while allowing credentials (any site can then read logged-in data, which I tested with a cookie in curl), allowing null, loose suffix checks like endsWith("example.com"), and treating CORS as if it were authentication. A safe setup uses an allow-list of exact origins, Vary: Origin, and credentials only for those.

JWT or server-side sessions? (Senior signal)

Both work if done right. A session stores state on the server, with only a random ID in the cookie. It is easy to revoke (delete the session), and the cookie can be HttpOnly. A JWT carries signed claims, so servers do not need a lookup, which is handy across many services. The cost is that revoking is hard before it expires, so I keep the lifetime short and use refresh tokens. For a normal website with one backend I prefer sessions. If I use JWTs I fix the algorithm on the server, check exp, iss and aud, keep the secret out of the code, and don't store sensitive data in the payload (it is only signed, not hidden). I avoid localStorage for tokens in a sensitive app because XSS can read it (Lesson 3).

An API key was committed to a public repository. What do you do? (Must know)

I rotate it first: revoke the key at the provider and create a new one, because the old key is already public (bots scan GitHub within moments, and my gitleaks test showed that deleting the file in the next commit does not remove it from history). Then I check the provider's logs to see if it was used. Then I remove it from the code and load the new key from the environment or a secrets manager. Cleaning the history is optional and last. Then I add push protection or a pre-commit scan so it does not happen again, and I write a short note on what happened. See Lesson 5, Lesson 6 and the incident table above.

What is defence in depth? (Must know)

It means several independent layers, so that one failure is not the end. For example, for SQL injection: parameterised queries (the main fix), input validation, a database account with the least privilege, an error handler that hides details, a WAF or logging that notices attacks. For SSRF: a host allow-list, no redirects, an IP check at connect time, and network rules that stop the server reaching internal services. If one layer has a bug, the next one still helps. It comes together with least privilege and "fail closed" (Lesson 0).

What is SSRF and how do you prevent it? (Senior signal)

SSRF is when an attacker makes my server fetch a URL they chose, so the server reaches internal things the attacker could not, such as an admin service or the cloud metadata address. I prevent it with an allow-list of hosts when I can, only http and https, no redirect following (or re-check every hop), and when users may give any public URL I resolve the name, check the IP against private ranges, and make that check happen in the connection itself, to defeat DNS rebinding. I also restrict the network for the fetching code and use IMDSv2 on AWS. In my toy a plain allow-list still leaked through a redirect, so I always test the redirect case (Lesson 7).

How do you secure an npm project? (Must know)

I commit the lock file and use npm ci in CI so the tree is exactly what was reviewed. I turn off install scripts by default (ignore-scripts=true) and allow the few that need it. I run npm audit in CI and use Dependabot for small weekly updates with a short cooldown, because a quickly removed bad version should not reach me. I check new packages before adding them: name, maintainers, size of the tree, install scripts. I protect publish rights with 2FA, preferably a hardware key or passkey, and I keep CI tokens short-lived with least privilege and pin actions to commit hashes (Lesson 6).

What is threat modelling and how do you do it quickly? (Senior signal)

It is asking early "what could go wrong?" so I can fix the design before writing code. I use four questions from the Threat Modeling Manifesto: what are we working on, what can go wrong, what are we going to do about it, did we do a good enough job. In practice: I draw a simple diagram with users, services, data stores and third parties, mark the trust boundaries, then go through each arrow with STRIDE (spoofing, tampering, repudiation, information disclosure, denial of service, elevation of privilege). I turn the answers into tickets, and I repeat when the design changes. Ten minutes at the whiteboard can find the same thing a penetration test would find later at a higher price.

You review a pull request. What security things do you look for? (Must know)

I look for new doors. New inputs: does the data reach a query, a command, HTML or a file path? New routes: is there authentication, and is there an ownership check, not only a login check? New outbound calls: can a user choose the URL? New dependencies and workflow changes: a package with install scripts, an action pinned to @main. I look for secrets and personal data in code and logs, error messages that show internals, missing rate limits on expensive or login routes, and CORS or cookie settings that got looser. I also look at deleted lines, since a removed check is a classic bug, and I ask for a test that fails if the fix is removed.

What is the difference between SAST, DAST and SCA? (Must know)

SAST reads source code without running it and looks for dangerous patterns (Semgrep, CodeQL). DAST attacks a running app from outside (OWASP ZAP). SCA checks my dependencies against known vulnerabilities (npm audit, Dependabot). Secret scanning (gitleaks) is a fourth kind. None replaces review: SAST cannot see a missing authorization check, which I showed by adding an IDOR route that Semgrep did not flag. DAST can't see code no request reaches. SCA only knows known problems. I use them together and for what they are good at.

Authentication vs authorisation? (Must know)

Authentication answers "who are you?" (login, tokens, MFA). Authorisation answers "what may you do?" (roles, ownership, permissions). Most real breaches are in the second: the user is properly logged in, and the server never checks whether this user may touch this object. That is why Broken Access Control is number 1 in the OWASP Top 10:2025 and why BOLA is API1. I check authorisation on the server, on every request, with deny by default (Lesson 4).

Hashing, encryption and encoding: what is the difference? (Senior signal)

Encoding (Base64, URL encoding) changes the format and is reversible by anyone, so it is not secret. Encryption hides data with a key, and can be reversed by someone with the key (AES-GCM for data at rest). Hashing is one-way: the same input gives the same output, and you can't get the input back. For passwords I use a slow, salted password hash. For checking file integrity I use a fast hash like SHA-256. A frequent mistake is to call Base64 "encryption" or SHA-256 "password security" (Lesson 5).

You find a vulnerability in production. Walk me through your first hour. (Senior signal)

I tell my lead and assign roles, so there is one incident lead and one note-taker. I contain first: turn off the feature, revoke the token or block the path, but I don't delete evidence. Then I rotate any secret that might be exposed. I save logs and snapshots with times. I use the logs to see how far it went and who or what was affected. I tell the people who need to know (management, then users or regulators if needed, with help from the lead or legal), with facts and without guessing. Then I plan the real fix with a test. Afterwards I write a blameless post-mortem with actions that have owners and dates (Team Lead Lesson 4).

A scanner says "high severity, 14 findings". The team wants to turn it off. What do you do? (Trap)

Turning it off is the wrong end; so is fixing all 14 in a hurry. I triage: which are real, which are false positives, and which can be reached by users. I fix the real ones that matter, suppress the false ones with a comment that says why, and set the check to fail only at a level the team will really act on. The goal is a scanner that is green most of the time and red for good reasons. A check everybody has learned to ignore is worse than none, because people think they are covered.

"We use HTTPS, so we are secure." True? (Trap)

No. HTTPS protects the data in transit from people on the network: they can't read or change it. It says nothing about what my server does with the data. SQL injection, XSS, broken access control, a leaked key and a bad package all work perfectly over HTTPS. It is necessary, not enough (Lesson 5).

Why is "security through obscurity" a bad plan? (Trap)

Hiding how something works is not a protection, because attackers can often find out (a URL guessed, a JavaScript bundle read, a name found in the traffic). A hidden admin URL like /admin-9xk2 is not authorisation. Obscurity is fine as an extra layer, but real protection is a check that still works when the attacker knows everything except the key or password. This is the idea behind Kerckhoffs's principle: a system should be secure even if everything but the key is public.

Key takeaways

  • Review for doors: new inputs, routes, outbound calls, packages and permissions. Use the checklist table, and also look at deleted lines.
  • Four kinds of scanners, four blind spots. SAST (Semgrep, CodeQL) misses missing checks; DAST (ZAP) misses code no request reaches; SCA (npm audit) only knows known problems; secret scanning (gitleaks) needs a recognisable shape. None replaces a reviewer and a test.
  • Deleting a secret does not remove it. My gitleaks run found the token in history after the file was gone. Rotate first.
  • Run cheap checks automatically in a workflow with pinned actions and minimal permissions, and make sure someone reads the failures.
  • Threat model in ten minutes with four questions and STRIDE. Fix the design before the code.
  • First hour of an incident: declare, contain, rotate, preserve logs, scope, tell people, plan the fix. Then a blameless post-mortem with owned actions. Write the plan before you need it.
  • Be a security champion: ask the question, keep the checklist, make the safe way the easy way, write the test with the fix, and say thanks to people who find holes.
  • Only test what you own or may test.

What's next

This is the last lesson of the series. To go deeper, pick the area where your work is:

And then practise: add the workflow from this lesson to a real project, write a one-page runbook for "a key was leaked", and ask one security question in your next pull request review. Revisit Lesson 0 after a month and see how much more of the map you can fill in.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Related lessons

Part of the study guide

Related series