What I learned studying product security as an engineer

September 9th, 2026 - 12 min read

Abstract attack path from untrusted input to a powerful sink, crossing a broken trust boundary

I can ship product systems. For a long time I could not walk into a messy repo, a noisy scanner report, or a design review and feel what mattered first.

So I studied product security the long way - not to become a full-time red-teamer overnight, but to stop guessing when someone asked:

Is this safe enough to ship?

The surprising lesson was not another list of CVEs. It was this:

Finishing the course and still freezing on the job is usually an indexing failure, not a memory failure.

You stored answers under the chapter they were taught in. Real work does not arrive as "chapter 12: SSRF." It arrives as a situation.

Real work arrives as situations

Courses file knowledge by topic:

  • Phase 1: web vulns
  • Phase 5: cloud
  • Phase 8: LLM stuff

Work files knowledge by trigger:

  • "There is an id in the URL"
  • "The server fetches a URL the user supplied"
  • "A scanner just returned 400 findings"
  • "We have one afternoon with this repo"

Experts are not people with more facts. They are people whose facts are clustered around a few deep principles and indexed by the situation that fires them. A doctor does not recall "chapter on sepsis." A presentation walks in and a whole script fires at once.

I needed that for security.

Reading a summary changes almost nothing. The useful version of this material is an instrument you run: scripts, checklists, a recall deck, a drill with a written verdict, and a gap journal. If you only read, you still have a textbook in your head. Textbooks do not triage.

The whole field on one page

Every bug I studied walks the same road. Every defense is a place you stand on it.

attacker-controlled input
  query, body, header, cookie, path, filename,
  a row a user stored yesterday, a dependency,
  a base image, a model's own output
        |
        v  crosses a TRUST BOUNDARY
        |
        v  propagates (assign, concat, pass, return)
        |
        v  arrives at a SINK
           SQL / shell / filesystem / HTML /
           template / XML / deserializer /
           URL fetcher / LLM tool call
        |
   no matching sanitizer in between  ->  vulnerability
        |
        v  chained to a second finding
        |
        v  compromise, sized by what you had already permitted

One sentence:

Untrusted data reaches a powerful operation with nothing matching in between, gets chained, and the damage is whatever you had already permitted.

When I have nothing else, I ask the question the page collapses into:

What untrusted thing reaches what powerful thing, and what stands between them?

That question is boring on purpose. Boring is what survives pressure.

Ten principles that generate the rest

I stopped trying to memorize fifty named bugs as fifty separate things. Most of them are generated by a short list of principles. A few that changed how I review code:

  1. A vulnerability is a path, not a bad character. Source → boundary → sink, with no matching control on the path.
  2. Data becomes code when data and command share one string. The fix is two channels: parameterized query, argv list, constant template with variables passed apart - not a "cleaner string."
  3. You cannot enumerate bad input. Prefer describing good input. Best of all, verify the outcome (resolved path, resolved IP, exact-match URL).
  4. Authentication is not authorization. Knowing who is calling never answers whether this object is theirs.
  5. A valid value is not proof of intent or ownership. Re-verify at the sink.
  6. The client runs on the attacker's machine. Hidden fields, disabled buttons, JS validation, isAdmin in the body - all suggestions.
  7. Kill the class, not the instance. One safe API + one lint rule + one CI gate beats infinite patched lines.
  8. Severity is not risk. Reachability decides.
  9. Severity is not additive along a path. It multiplies.
  10. Assume the boundary fails and size the blast radius in advance. Least privilege is how you choose what it costs when they get in.

Number 4 and number 8 alone would have saved me months of false confidence.

Illness scripts for engineers

The instrument that turned "I read about IDOR" into "I recognise IDOR" was a set of scripts - situations written the way reality presents them:

TRIGGER   what you see first
UNDER     the mechanism, stated exactly
SIGNS     what else you will see if this is really it
MEANS     the class of problem
MOVES     your first three moves, in order

A recognition table helps more than a syllabus:

You seeIt is probably
an id in the URL or bodyIDOR / BOLA
the server fetches a URL you gave itSSRF
your input comes back on the pageXSS
a ' changes results or errorsSQLi
a role / price / flag in the requesttampering / mass assignment
a scanner dumped hundreds of findingstriage overload
a CVSS 9.8 in a dependencyreachability question
an Action: "*" in a policyover-broad IAM
an LLM with toolsprompt injection

The boring bug that eats companies

Here is the script that shows up constantly in product work:

TRIGGER   GET /api/account/123  or  {"user_id": 5} in a body
UNDER     The server fetches by id after checking you are logged in,
          but never that this object is yours. Auth ≠ authz.
SIGNS     Sequential ids. Full object in the response.
          Change the id by one → someone else's data with a 200.
MEANS     Broken access control (IDOR / BOLA). #1 web risk, #1 API risk.
MOVES     1. As user A, fetch A's object, then change the id to B's.
          2. If B's data returns, confirmed. No fancy payload needed.
          3. Fix: scope the query. Deny by default.

In code, the difference is almost insultingly small:

// looks "authenticated"... still broken
app.get('/api/invoices/:id', requireAuth, async (req, res) => {
	const invoice = await db.invoice.findUnique({
		where: { id: req.params.id },
	})
	return res.json(invoice)
})
// ownership is the actual control
app.get('/api/invoices/:id', requireAuth, async (req, res) => {
	const invoice = await db.invoice.findFirst({
		where: {
			id: req.params.id,
			ownerId: req.user.id,
		},
	})
	if (!invoice) return res.status(404).end()
	return res.json(invoice)
})

Same token. Same endpoint shape. Completely different security story.

If your review checklist only asks "is the user logged in?", you are grading authentication and calling it access control. Attackers love that confusion. So do scanners that cannot understand your ownership model.

Checklists are for what falls out of your head

I used to think checklists meant "follow every step like a recipe." The useful version is do-confirm:

  1. Do the work from your own head.
  2. At a named pause point, stop.
  3. Run a short list of killer items - the things that actually get skipped under pressure.

A complete list is a useless list. Past roughly a minute at a pause point, you stop reading it.

Before I call a handler safe, I now ask things like:

  • Is every request value treated as untrusted?
  • For every id, do we check ownership, not just login?
  • Does any client field bind straight onto privileged model fields?
  • Can I trace every sink back to a constant or a sanitized source?

Before I file a finding:

  • Exact location (file, line, function)
  • Mechanism in one sentence
  • Concrete impact
  • Severity by reachability in this system, not raw CVSS
  • Minimal proof against a system I own

A finding without a mechanism is an alarm, not a finding.

The traps that survive knowing the facts

Competent engineers get this wrong in predictable ways. The value is not the label - it is the tell, the observable that exposes it.

Looks likeActually isThe tell
"We strip ../ and reject quotes"Defending spellings you thought ofFix inspects input patterns instead of verifying outcomes
"We set HttpOnly, XSS remediated"Blast-radius shrink, XSS still runsRemediation names a cookie flag, not output encoding
"Token is valid, so access is controlled"Authn without authzObject fetched by request id with no ownership clause
"Form validates it / button is disabled"Client-side theaterCheck exists only in the browser
"SAST was clean"Clean for the rules you ranNo manual review, no custom rules for your dangerous helpers
"CVSS 9.8, drop everything"Abstract severity mistaken for riskFix order matches raw CVSS with no reachability check
Scoring findings one by oneDeleting the chain the attack is made ofTwo "mediums" never get considered as one path

That last one is vicious. Scanners and tired humans score findings in isolation. Attackers do not.

Numbers worth knowing cold

A few constants that now set off alarms before I "do analysis":

  • Broken access control is OWASP's #1 web risk. The boring id-in-the-URL bug, not the clever exploit, is the common case.
  • BOLA is the same failure at API speed across millions of ids.
  • Typical apps are 80-95% third-party code by line. Most of your surface is code you did not write.
  • Most cloud breaches are an access / identity story, not a novel zero-day.
  • A leaked cloud key on public GitHub has a minutes-scale time-to-exploit.
  • Capital One 2019 is still the clearest teaching story: SSRF → metadata → over-broad IAM. A scoped role makes stolen credentials nearly worthless.

Calibration matters. If your mental model of "how breaches happen" is Hollywood zero-days, you will under-invest in ownership checks, secret handling, and IAM shape.

Judgment is a drill, not a vibe

The proof that this is working is not a certificate. It is a drill:

  1. Get ten mixed findings.
  2. Rank them yourself.
  3. Write why.
  4. Diff against an experienced reviewer's ranking.
  5. Journal every gap.

The gap journal line is deliberately tiny:

I said X, the reviewer (or reality) said Y, because Z.

Examples that actually teach:

I ranked the CVSS 9.8 near the top; reviewer ranked it near the bottom,
because the vulnerable function is never called. Reachability, not severity.

I scored two mediums separately; reviewer chained them into a payment-token
harvest, because severity multiplies along a path.

I said "delete the committed key"; reviewer said "rotate first",
because git history keeps it forever on every clone.

A fact you got right teaches almost nothing. A gap teaches exactly one thing, precisely.

Then wire if-then triggers so the manual fires without a motivational speech:

  • If I see an id in a URL or body, then check ownership before anything else.
  • If the server fetches a URL I can influence, then think SSRF and metadata endpoints before I argue about feature design.
  • If a scanner hands me a pile, then rank by reachability, not CVSS.
  • If I find a committed secret, then rotate before I tidy the file.

What changed in how I build products

I still care about performance, clarity, and shipping. Product security did not replace those. It gave them a sharper edge:

  • I design ownership into queries and domain models early, not as an afterthought ticket.
  • I treat client-supplied privileged fields as hostile by default.
  • I ask for blast radius when someone says "just make this service role temporary admin."
  • I triage scanner noise without pretending a green pipeline means a safe product.
  • I write findings (and PR review comments) with mechanism and proof, not vibes.

The operating manual I keep now is not a second brain stuffed with every CVE name. It is a small set of principles, situation scripts, short checklists, and a habit of writing down where my judgment was wrong.

That is what makes the knowledge survive contact with a real codebase.

If you are an engineer studying product security: do not only collect chapters. Re-file what you learn under the situations that will actually walk into your day. Then run a drill. Then keep a gap journal.

That is the difference between finishing a course and being able to do the work.

Doctor discovery product graphic

Doctor Finder & Instant Booking

Help patients find specialists near them and book into real hospital systems - so your marketplace captures demand instead of losing it to call centers.

See the business story
Secure video streaming graphic

Secure Video Hosting at Scale

Private streaming that feels first-party - with YouTube-backed storage and a lean middle tier designed for high concurrency and low cost.

See the business story
Hamidul Islam
Written by Hamidul Islam

Hamidul Islam is a product engineer focused on performance, systems thinking, and the path from hardware into software. He builds product systems that stay fast under pressure and shares what he learns here.

Learn more about Hamidul

Have a question about this article?

Send me a note via the contact page or schedule a call.

Contact me

If you found this article helpful.

You will love these ones as well.