Security for Developers
The Security Mindset: Threats, Attack Surface and the OWASP Top 10
How attackers think, what to protect, threat modelling in four questions, defence in depth, least privilege, and a map of the OWASP Top 10 so you know where each lesson fits.
On this page
- Words used in this lesson
- What security is, and what it is not
- How attackers think
- Attack surface: count the doors
- Threat modelling in four questions
- Defence in depth: assume one layer will fail
- Least privilege: the smallest power that works
- Two more habits: fail closed and keep it simple
- The OWASP Top 10, in plain words
- Where to learn more (the sources, and what each is good for)
- Try it
- Interview questions
- Key takeaways
- What's next
In one line: security is not a list of tricks. It is a way of thinking: find what is worth protecting, ask how someone could abuse it, add several small layers so one mistake is not a disaster, and give every part of the system only the power it needs. Once you think this way, the OWASP Top 10 stops being a scary list and becomes a map.
Most developers meet security as a surprise. A scanner finds a problem the day before release, or an email arrives that says "your users' data is on a forum". This series tries to change that. We will not memorise attacks. We will learn the few ideas behind them, then run small examples on our own computer so you can see the problem and the fix with your own eyes.
This is lesson 0 of a nine-lesson series. You do not need to know security before you start. You only need basic JavaScript and a rough idea of what a web server does. The next lesson, Injection, shows a real SQL injection and how to stop it. In this lesson there is no big attack yet. First we set up the way of thinking, then a small threat model, then a map of the OWASP Top 10 so you know where every later lesson fits.
If you want the browser side of these ideas, Browser Lesson 7 (defence in depth) and Browser Lesson 3 (where to keep tokens) are good companions. For the interview angle on server attacks see Full-Stack Lesson 31.
What I ran / checked. The code in this lesson ran on Linux with Node.js v22.22.0, using the built-in node:sqlite module (SQLite 3.50.4) and the built-in Node permission flag. I pasted the real output. Sources I read (in my own words below, nothing copied): the OWASP Top 10:2025 introduction and the pages for A01, A05 and A06 from the OWASP/Top10 repository (raw files on GitHub); the OWASP Cheat Sheet Series "Threat Modeling Cheat Sheet", which quotes the four questions of the Threat Modeling Manifesto and uses the STRIDE method. Could not reach: owasp.org itself, PortSwigger Web Security Academy and MDN were blocked from my machine, so I did not read them for this lesson, and the OWASP Web Security Testing Guide was only confirmed to exist. I also could not read Adam Shostack's own pages, so the four questions come from the OWASP cheat sheet and not from Shostack directly. I did not verify any GitHub star counts, so I give none. The OWASP Top 10 list below was checked against the 2025 edition files. A web search result said that edition was still being finalised in places, so check the official page before you quote it in a report.
Words used in this lesson
- An asset is something worth protecting: user data, money, accounts, your server, your reputation.
- An attacker is anyone who tries to use your system in a way you did not intend. It may be a stranger, a user who is curious, or a bot.
- A vulnerability is a weakness in the system, such as a query that trusts user input.
- A threat is something bad that could happen because of a weakness, such as "someone reads another user's notes".
- An exploit is the actual way of using a vulnerability.
- Attack surface is the total set of places where an attacker can reach your system: every URL, form field, header, file upload, API key, dependency and login.
- A trust boundary is a line where data moves from a place you control to a place you do not (or the other way). The browser to your server is a trust boundary.
- Threat modelling is the habit of thinking about what can go wrong before it does.
- Defence in depth means using several layers of protection, so if one fails, another still holds.
- Least privilege means giving each user, program or key only the permissions it really needs.
- The CIA triad is three goals: confidentiality (only the right people can read), integrity (nobody can change data wrongly) and availability (the service works when needed).
- OWASP is the Open Worldwide Application Security Project, a non-profit that publishes free security guides. The OWASP Top 10 is its list of the ten most serious kinds of web app risk.
What security is, and what it is not
Security is about stopping people from doing things you did not allow. That is all. A bug is when the app does the wrong thing by accident. A security problem is when someone makes the app do the wrong thing on purpose.
The CIA triad helps you ask the right question about any feature:
| Goal | Plain question | Example failure |
|---|---|---|
| Confidentiality | Can someone read what they should not? | A user sees another user's notes |
| Integrity | Can someone change what they should not? | A user edits the price in the request |
| Availability | Can someone stop others from using it? | One script sends a million requests and the site is down |
Three beliefs make developers unsafe. Let us name them now:
- "Nobody would attack us, we are small." Attacks are mostly automatic. Bots try the same URLs and the same passwords on every server they can find. They do not care how big you are.
- "We have a firewall / we use HTTPS, so we are safe." HTTPS protects data on the way. It does nothing about a bad query inside your server. A firewall does not understand your app's logic.
- "Security is the security team's job." The person who writes the line
db.query("..." + name)is the person who decides if there is a hole. Review and tools help, but you write the code.
How attackers think
An attacker does not read your code like a friend. They look at your app and ask, "what happens if I send something unusual?" Normal users do the expected thing. Attackers do the unexpected thing, over and over, with tools.
They change things you assumed could not change:
- A form field. Your page has a
<select>with three options. The attacker sends a fourth value, because the browser is not the one in charge. - A URL number.
/notes/17is yours, so they try/notes/18. - A header or a cookie. You trusted
X-User-Id, so they send their own. - The order of steps. You assumed "step 3 comes after step 2", so they jump to step 3.
- The size and type of data. You expected a name. They send 10 million characters, or a JSON object where you expected a string (we will do this in lesson 1).
The one rule that covers all of it: everything that comes from outside your code is untrusted. The browser, the URL, the headers, the cookies, the files, even your own mobile app. Any of these can be changed by the person on the other side. Checking only in the front-end (for example with required in HTML) is a help for honest users and no protection at all against a script.
This is the same idea as in Backend Lesson 3 (validate at the edge). Security just takes it more seriously: validation answers "is this the right shape?", and security also asks "even if the shape is right, is this person allowed, and is this data safe to put into a query or a page?"
Attack surface: count the doors
Every way into your system is a door. More doors, more chances that one is open. Let us count them for a tiny note-taking app:
- The login form (username, password).
- The "create note" form (title, body).
- The URL
/notes/:idto read a note. - The "search notes" box.
- The session cookie.
- The database and its password.
- The 400 npm packages in
node_modules. Each one runs code on your server. - The admin page, and the CI system that deploys your code.
A good habit is to make the surface smaller before you try to defend it. Remove the endpoints you do not use. Turn off the debug page. Do not install a package for one small function. A door that does not exist cannot be broken.
Then watch the trust boundaries: every time data crosses from outside to inside, treat it as unsafe until you check it. That is where most of the series is spent.
Threat modelling in four questions
Threat modelling sounds heavy. It is not. The Threat Modeling Manifesto reduces it to four plain questions, and the OWASP cheat sheet uses the same four:
- What are we working on? Draw the system: who uses it, what parts exist, where data goes.
- What can go wrong? Think like the attacker, one part at a time.
- What are we going to do about it? For each problem: remove it, reduce it, accept it, or hand it to someone else.
- Did we do a good enough job? Look again. Did we cover the important things? Did the system change?
A popular helper for question 2 is STRIDE, six prompts that remind you of kinds of threats. The OWASP cheat sheet uses it as its example method (it also says other methods exist, so STRIDE is a tool, not a law):
| Letter | Threat | The question it asks |
|---|---|---|
| S | Spoofing | Can someone pretend to be another user? |
| T | Tampering | Can someone change data they should not? |
| R | Repudiation | Can someone do a thing and later say "it was not me", with no proof? |
| I | Information disclosure | Can someone read data they should not? |
| D | Denial of service | Can someone make it stop working? |
| E | Elevation of privilege | Can someone gain more power than they should have? |
A tiny threat model for a note-taking app
Question 1: what are we working on? A small app. Users sign up, log in, and keep private notes. A browser talks to a Node server. The server talks to a database. The files:
[Browser] --HTTPS--> [Node server] --> [Database] (user) boundary 1 | boundary 2 (notes, users) +--> [npm packages]What are the assets? The notes (private text), the password hashes, the session cookies, and the server itself. What is the worst case? One user reads another user's notes. Write that down. It tells you what to protect first.
Question 2: what can go wrong? Go through STRIDE for each part. We keep each line short:
| Part | STRIDE | What could go wrong | Which lesson helps |
|---|---|---|---|
| Login | S | Someone guesses passwords or steals a session | Lesson 3 |
Note URL /notes/:id | I, E | User A changes the number and reads user B's note | Lesson 4 |
| Search box | T, I | Search text is pasted into SQL, and returns every note | Lesson 1 |
| Note body shown on page | T | A note contains a script that runs in other people's browsers | Lesson 2 |
| Delete button (cookie login) | T | A bad site makes the user's browser send "delete" | Lesson 2 |
| Admin actions | R | Nobody can tell who deleted a note, because there are no logs | Lesson 8 |
| API keys in the repo | I | A key leaks through git or logs | Lesson 5 |
| npm packages | T, E | A package update contains bad code | Lesson 6 |
| Server and upload | D, E | Huge uploads, no rate limit, server fetches attacker URLs | Lesson 7 |
Notice something: we have not written one line of attack code, and we already have a list of work, in order of importance. That is the value of threat modelling. It is cheap, and you can do it on paper in 30 minutes.
Question 3: what are we going to do about it? For each row choose one: fix it (parameterised queries), reduce it (rate limits), accept it (a note-app does not need bank-level fraud checks) or transfer it (use a login provider instead of writing your own). Write down which, so the decision is clear later.
Question 4: did we do a good enough job? Review the model when the system changes: a new endpoint, a new third-party service, a new role. A threat model is a living page, not a one-time document.
Do not skip question 4. A model that was true last year is wrong after you add file uploads. Many problems in real systems come from a feature added later that nobody put back through question 2.
Defence in depth: assume one layer will fail
Developers make mistakes. So do reviewers. A system that is safe only if every line of code is perfect will fail one day. Defence in depth means putting a second and third layer behind the first, so one mistake is not the end.
Think of a house: a lock on the door, a fence, a dog, and a safe inside. A thief must pass all four. Our note-taking app might use:
- Validate input (reject nonsense early).
- Use parameterised queries (so input can never become SQL).
- Give the database user only the rights it needs (so even a bug cannot drop a table).
- Log and alert (so someone notices the attempt).
Let us prove layer 3 with a real run. Our search feature only needs to read notes, so its database connection can be opened as read-only. I deliberately wrote layer 1 and layer 2 wrongly (the query pastes the search text in), to see what layer 3 does.
import { DatabaseSync } from "node:sqlite";const search = new DatabaseSync("notes.db", { readOnly: true });const bad = (term) => search.prepare(`SELECT body FROM notes WHERE owner = 'ada' AND body LIKE '%${term}%'`) .all().map(r => r.body);console.log("normal search:", bad("milk"));console.log("injected read:", bad("zzz%' OR owner = 'bob' OR body LIKE '%"));try { search.exec("DROP TABLE notes"); } catch (e) { console.log("DROP:", e.message); }try { search.exec("UPDATE notes SET body='hacked'"); } catch (e) { console.log("UPDATE:", e.message); }normal search: [ 'buy milk' ]injected read: [ 'buy milk', 'secret plan' ]DROP: attempt to write a readonly databaseUPDATE: attempt to write a readonly databaseRead the output slowly. The injected search did leak bob's note (layer 2 was broken, and we will fix this kind of hole properly in lesson 1). But the attacker could not change or delete data, because layer 3 would not allow writes. One bug became a smaller bug. That is the whole idea. (The DROP and UPDATE lines above call the connection directly to show that the database refuses writes. A prepared statement in node:sqlite also runs one statement at a time, which is another small help.)
Defence in depth is not a reason to leave layer 2 broken. The injected read above is still a data leak. Layers reduce the damage, they do not replace the fix.
Least privilege: the smallest power that works
Least privilege says: every user, process and key should be able to do only what its job needs. If it is stolen or buggy, the damage stays small.
Examples you can use tomorrow:
- A web server's database user should not be the database owner. Give
SELECT,INSERT,UPDATEon the tables it uses and nothing else. Run migrations with a different, stronger user. - A reporting script gets a read-only connection (as above).
- An API key for sending email should not also be able to read your user table.
- A normal user cannot call admin URLs. This is a check on the server, not a hidden button (lesson 4).
- A GitHub Actions token should have only the permissions the job uses (lesson 6).
Node itself has a switch for this idea. The permission model can limit what a script is allowed to touch. Here is a tiny script that reads a system file:
import { readFileSync } from "node:fs";try { console.log(readFileSync("/etc/hostname","utf8").trim()); }catch(e){ console.log(e.code, "-", e.message.split("\n")[0]); }-- no flags:vm-- with --permission:ERR_ACCESS_DENIED - Access to this API has been restricted. Use --allow-fs-read to manage permissions.I ran the second one as node --permission --allow-fs-read=/tmp/sec-lab-0 fs.mjs. The script may read only that one folder, so reading /etc/hostname is denied. If a bug in your code were tricked into reading a secret file, this flag would stop it. It is a seat belt, not a replacement for good code. It is also a newer Node feature, so check your Node version and the docs before you rely on it in production.
The most common least-privilege mistake is the "temporary" admin key that becomes permanent. If a key has more power than its job, rotate or reduce it now.
Two more habits: fail closed and keep it simple
- Fail closed (deny by default). When something goes wrong or you are unsure, say no. If a permission check throws an error, the answer is "not allowed", not "allowed". Most access control bugs (lesson 4) come from code that only says no in the cases the developer remembered.
- Keep it simple. Every extra feature and library is a new door. Complex code hides bugs. A boring, small design is easier to review, and easier to secure.
The OWASP Top 10, in plain words
OWASP publishes a list of the ten most serious risks for web applications, built from real data on tested applications and from a survey of practitioners. It is an awareness document. It does not list every possible bug. Think of it as the ten places most worth looking first.
I read the introduction of the 2025 edition (the eighth version of the list) and confirmed these ten names:
| # | Category (2025) | In plain words |
|---|---|---|
| A01 | Broken Access Control | A user can do or see something they are not allowed to |
| A02 | Security Misconfiguration | Unsafe settings, default passwords, debug pages, missing headers |
| A03 | Software Supply Chain Failures | Danger from the packages, build tools and systems you depend on |
| A04 | Cryptographic Failures | Weak or missing encryption and bad handling of secrets and keys |
| A05 | Injection | Untrusted data becomes part of a command or query (SQL, OS, and also cross-site scripting) |
| A06 | Insecure Design | The design itself has no defence, for example no threat modelling |
| A07 | Authentication Failures | Weak login, weak sessions, no brute force protection |
| A08 | Software or Data Integrity Failures | Trusting code or data without checking it was not changed |
| A09 | Security Logging & Alerting Failures | No logs or alerts, so attacks go unnoticed |
| A10 | Mishandling of Exceptional Conditions | Errors and odd situations handled in unsafe ways |
Things worth noticing, from the pages I read:
- A01 Broken Access Control is still number one. In the OWASP data, every application tested had some form of it. The page lists SSRF (a server tricked into fetching a URL for the attacker) and CSRF among its related weaknesses.
- A05 Injection says it includes cross-site scripting (very common, usually lower impact) and SQL injection (less common, usually very high impact). This is why lessons 1 and 2 are neighbours.
- A06 Insecure Design asks for threat modelling and secure design patterns before code is written. That is what you just did in the section above.
- A03 (supply chain) and A10 (exceptional conditions) are the changes from the 2021 list that people talk about most. I did not read the 2021 list again for this lesson, so for a full list of what moved, read the "what changed" part of the official introduction.
Top 10 lists change between editions. Interview answers and security reports should say which edition you mean. "A03 is Injection" was true in the 2021 numbering and is not true in 2025, where Injection is A05.
Where each category lives in this series
| OWASP 2025 | Where we cover it |
|---|---|
| A01 Broken Access Control | Lesson 4 (IDOR, roles, mass assignment); CSRF in Lesson 2; SSRF in Lesson 7 |
| A02 Security Misconfiguration | Headers in Lesson 2; servers, CORS and errors in Lesson 7 |
| A03 Software Supply Chain Failures | Lesson 6 |
| A04 Cryptographic Failures | Lesson 5 |
| A05 Injection | SQL, command and NoSQL in Lesson 1; XSS in Lesson 2 |
| A06 Insecure Design | This lesson, and the review checklist in Lesson 8 |
| A07 Authentication Failures | Lesson 3 |
| A08 Software or Data Integrity Failures | Lesson 6 (CI and updates), tokens in Lesson 3 |
| A09 Security Logging & Alerting Failures | Lesson 8 |
| A10 Mishandling of Exceptional Conditions | Error messages in Lesson 7, fail-closed habit above, and Lesson 8 |
This mapping is my own teaching map, so one category can appear in two lessons. It is not an official table.
Where to learn more (the sources, and what each is good for)
- OWASP Top 10 for the awareness list above.
- OWASP Cheat Sheet Series (a large public collection of short "how to do it safely" pages: SQL injection prevention, XSS prevention, CSRF prevention, CSP, security headers, threat modelling and many more). Lessons 1 and 2 follow it. It is the best place to look up "what is the safe way to do X".
- OWASP Web Security Testing Guide is a long guide for testers: how to check each kind of weakness.
- PortSwigger Web Security Academy has free interactive labs where you attack a deliberately weak app. I could not open it from my machine while writing, but it is widely used for practice.
- MDN Web Security documents how the browser side works (cookies, CSP, same-origin).
Only attack systems you own or have written permission to test. All attacks in this series run against a small toy app on your own computer. Testing someone else's website without permission can be a crime, even if you "did not cause damage".
Try it
- Take any small project you have (or a to-do app idea). Draw the boxes: browser, server, database. Mark the trust boundaries.
- Write the list of assets. What is the worst thing that could leak?
- Go through STRIDE for one box. Write at least one "what can go wrong" per letter, even a silly one.
- Count your attack surface: how many URLs accept input? How many npm dependencies are in
package.json? - Find one place where a part of your system has more power than it needs (a database user, a token, an admin flag). Write what the smaller power would be.
- Open the OWASP Top 10 page for the edition you want and pick the category that worries you most for this app.
Answers
There is no single right answer for these, but check yourself against these points:
- The browser to server line is always a trust boundary. The server to database line often is too, if the data came from the user.
- For a typical app, the worst case is "one user reads or changes another user's data", or "someone gets admin".
- Spoofing is about identity (login). Tampering is about changing data. Repudiation is about missing logs. Information disclosure is about reading. Denial of service is about overload. Elevation is about gaining rights. If you could not think of one, look at what happens if a user sends a very large or odd value.
- Most small apps have more doors than their authors expect, mainly through dependencies.
- Usually the answer is a database user with full rights, or a token that can write when it should only read.
- For most apps the honest answer is A01 (access control) or A05 (injection), because they are the most common.
Interview questions
What is threat modelling, and how would you do it on a small feature? (Must know)
Threat modelling is thinking about what can go wrong before it does. I use the four questions: what are we working on, what can go wrong, what are we going to do about it, and did we do a good enough job. For a small feature, say a "share note" link, I draw the flow on paper, mark where data crosses a trust boundary, and go through STRIDE for each step. For each threat I choose to fix, reduce, accept or transfer it, and I write the choice down. I do it again if the feature changes. It takes under an hour and finds design problems that are very expensive to fix later.
What is the difference between a vulnerability, a threat and an exploit? (Must know)
A vulnerability is the weakness, for example a query built by joining strings. A threat is what could go wrong because of it, for example "an attacker reads all users". An exploit is the actual technique or code that uses the weakness, for example sending ' OR '1'='1. Risk is how likely the threat is multiplied by how bad it would be. I fix the high-risk ones first.
Explain defence in depth with an example. (Must know)
It means several layers, so one failure is not the end. For database safety I would validate input, use parameterised queries, give the app a database user with minimal rights, and log odd queries. In my test, I made the query layer wrong on purpose and used a read-only connection. The attacker could still read a note that they should not, but could not change or delete data. So the layers made one bug smaller. I always add that this is not an excuse to leave the first layer broken.
What is the principle of least privilege? Give two examples from a web app. (Must know)
Each user, process and key gets only the permissions its job needs. Example one: the web app's database user can read and write the tables it uses, but cannot drop tables or change the schema, which a separate migration user does. Example two: a CI token that only needs to read the repository should not have permission to push or publish packages. If either is stolen or has a bug, the damage is limited.
What is "attack surface" and how do you reduce it? (Senior signal)
It is every place an attacker can interact with the system: endpoints, form fields, headers, uploads, dependencies, admin pages and build pipelines. I reduce it by removing what is not needed (unused routes, debug pages, packages), by putting internal services off the public network, and by keeping one clear entry point where I validate and check access. I also watch it over time, because it grows quietly with every new dependency.
Why can't you rely on front-end validation for security? (Trap)
Because the attacker does not have to use my front-end. They can send requests straight to the server with curl or a script, with any values they like. Front-end validation helps honest users. The server must check again every time, for shape, for permission and for safe use of the data. A good answer also says that validation is not the same as protection from injection: even valid-looking text must be kept out of the code part of a query.
What is the number one risk in the OWASP Top 10 and what does it mean? (Must know)
In the 2025 edition it is A01 Broken Access Control, as it was before. It means the app lets a user do or see something they are not allowed to, for example by changing an ID in the URL. It is common because the check has to be written by hand on every route. I say which edition I mean, because the list changes: Injection is A05 in 2025.
Where does Injection sit in the 2025 list, and what does it include? (Trap)
It is A05:2025. It includes SQL, NoSQL, OS command and other injections, and the OWASP page also counts cross-site scripting in the category. A trap here is to say "A03 Injection", which was the 2021 numbering. I would also explain the shared idea: untrusted data gets mixed into something that is interpreted as code.
"We use HTTPS, so we are secure." Do you agree? (Trap)
No. HTTPS protects the data while it travels between the browser and the server, so nobody on the network can read or change it. It does nothing about what my server does with the data: a SQL injection, a missing access check, or a weak password policy all work over HTTPS. HTTPS is one necessary layer, not the whole answer.
A teammate says "this endpoint is internal, nobody can reach it, so no checks needed". What do you say? (Senior signal)
I say that "internal" is an assumption that tends to break. A proxy rule changes, a server-side request forgery makes our own server call it, a developer exposes it for a demo. I would ask what happens in the worst case if someone does reach it, and add the same basic checks (login, permission, input validation) with a lighter touch if needed. This is defence in depth and fail closed together.
How do you decide what to fix first? (Senior signal)
By risk: how likely it is and how bad it is. I start from the assets and the worst case in the threat model, so for a notes app, one user reading another's notes comes before a cosmetic header. Exploits that need no login and give access to all data come first. I also consider the cost of the fix, because a five-minute fix for a medium risk can go before a two-week fix for a medium risk. I write down what I choose not to fix, with a reason and an owner.
Key takeaways
- Everything that comes from outside your code is untrusted. This one rule explains most of web security.
- Count your doors (attack surface) and reduce them. Watch the trust boundaries.
- Threat modelling is four questions: what are we working on, what can go wrong, what will we do, did we do enough. STRIDE helps with the second one.
- Use several layers (defence in depth), and give each part the least privilege it needs. A read-only database connection stopped writes even when the query layer was broken.
- Fail closed: if unsure, deny.
- The OWASP Top 10:2025 has ten categories, with Broken Access Control at A01 and Injection at A05. Always name the edition.
- Test only systems you own or have permission to test.
What's next
Next: Injection, Lesson 1. You will break a tiny login with a real SQL injection, watch the exact SQL that was sent, and then fix it with parameterised queries. After that we look at command injection and NoSQL-style injection, and the rule behind all of them. Practise now: do the threat model on paper for one of your own projects before you read on.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- Securing LLM Applications: The OWASP Top 10 for LLM Apps in Code · AI Engineering
- Security and Auth Scenario Bank · Scenario Debugging
- Security in Depth for Full-Stack Developers · Full-Stack Questions
Part of the study guide
Related series