You were trained to make software work. Attackers spend their time making it do things it was never meant to. That gap between how a system should behave and how it can behave is where almost every vulnerability lives, and the whole skill is learning to hold both views in your head at once.
Take a profile photo upload. You think: the user picks an image and we save it. An attacker looks at the same endpoint and asks a different set of questions.
- What happens if I upload 10 GB?
- What if the filename is
../../etc/passwd? - What if this "image" is actually HTML with a script inside it?
- What if I drop the content-type header entirely?
None of those are how the feature is meant to be used. All of them are things the code has to survive. Get into the habit of asking "what if?" before you ship, not after someone else does it for you.
Three questions before you ship
Run any new endpoint through these:
- Who can reach this? Public, authenticated, admin only? And can someone slip past the gate you think is there?
- What data does it touch? Credentials, personal data, payment details? Sensitivity sets the severity.
- What if the input is hostile? Not malformed by accident. Crafted on purpose, by someone who has read your code.
Least privilege
Give every component the minimum access it needs to do its job, and nothing more.
- A read-only page gets a read-only database connection.
- A frontend API key gets no admin scope.
- A background job that sends email gets no access to billing data.
Things break eventually. When they do, least privilege decides whether you lose one component or the whole account.
Defence in depth
No single control holds every time, so you stack them and let each one cover the failures of the last.
- Input validation rejects obvious garbage at the boundary.
- Parameterised queries stop SQL injection even when validation misses something.
- Output encoding stops XSS even when bad data reaches the template.
- A Content Security Policy limits what runs even when encoding fails.
Any one of these can fail on its own. The point is that they rarely fail together.
There is no "secure"
There is only "secure enough, for this context, against these threats, right now." A personal blog and a banking app face completely different attackers, so they earn completely different amounts of your attention. Security is a series of risk decisions, not a finish line you cross once.
Habits worth building
- Read your error messages. Stack traces leak stack details, library versions, and file paths. Error handling is part of your attack surface.
- Question defaults. Frameworks ship sensible defaults, usually. Know which ones they don't ship.
- Track where state comes from. Who set this value, and can someone else change it before you read it?
- Treat every network as hostile, internal ones included. TLS everywhere.
- Log what matters. When something bad happens, you want to be able to answer "who did it?"
This mindset isn't paranoia, and it isn't about fear. It's a habit of curiosity you apply early and often: keep asking how the thing could be turned against you, and you will catch most of it before it ships.
