Application Security & Secure Code Review
Read unfamiliar code, find the real bug, ship the frictionless fix
The vulnerability classes that matter
After this you can identify and explain the high-impact web/app vuln classes on sight.
The secure-code-review onsite hands you unfamiliar code and watches what you flag first. Strong reviewers don't memorize CVEs - they recognize a handful of vulnerability classes and trace each one to its root cause in seconds.
A Security Software Engineer at Cursor is a builder who reasons about attack surface fluently. The screens reward pattern recognition: you should look at a line and feel the class it belongs to, then name the fix that kills the whole class rather than the single line. That instinct is what separates a security engineer from a linter.
Almost every web vuln you'll be shown collapses into a small number of families. Learn the root cause and the paved-road fix for each and you can review code you've never seen.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
How a senior triages findings: who can reach it times what they gain - not the tool's severity badge.
The injection familyuntrusted data crosses into a different interpreter
Injection bugs share one root cause: data the attacker controls gets concatenated into a string that some interpreter then executes - SQL, a shell, an HTTP client, a template engine. The fix is always to keep data as data. Parameterize, don't interpolate.
- Class
- SQL injection
- One-line root cause
- User input concatenated into a query string
- Paved-road fix
- Parameterized queries / prepared statements; never string-build SQL
- Class
- Command injection
- One-line root cause
- Input passed to a shell that re-parses it
- Paved-road fix
- Pass argv arrays to
exec, never a shell string; dropshell: true
- Class
- SSRF
- One-line root cause
- Server fetches a URL the attacker supplies
- Paved-road fix
- Allowlist destinations; block link-local/metadata IPs; resolve then validate
- Class
- Template injection
- One-line root cause
- User input rendered as template source, not data
- Paved-road fix
- Render with autoescaping on; never compile user strings as templates
| Class | One-line root cause | Paved-road fix |
|---|---|---|
| SQL injection | User input concatenated into a query string | Parameterized queries / prepared statements; never string-build SQL |
| Command injection | Input passed to a shell that re-parses it | Pass argv arrays to exec, never a shell string; drop shell: true |
| SSRF | Server fetches a URL the attacker supplies | Allowlist destinations; block link-local/metadata IPs; resolve then validate |
| Template injection | User input rendered as template source, not data | Render with autoescaping on; never compile user strings as templates |
Same shape every time: untrusted data treated as code in a second interpreter.
q = f"SELECT * FROM users WHERE email = '{email}'" # injectable
cur.execute("SELECT * FROM users WHERE email = %s", (email,)) # safeSSRF is the injection that hides in plain sight. "Fetch this webhook URL" or "import from this image link" lets an attacker point your server at 169.254.169.254 and read cloud metadata credentials. An allowlist of egress hosts beats any blocklist of bad IPs, because the blocklist always misses a representation (decimal IPs, DNS rebinding, IPv6-mapped).
XSS and CSRFthe browser-trust pair
XSS runs attacker JavaScript in a victim's session: stored (saved to the DB and served to everyone), reflected (echoed from the request) or DOM-based (client-side code writes untrusted data into the page). CSRF is the inverse - it rides the victim's existing session to make a state-changing request they didn't intend.
React/JSX and modern templating autoescape by default, which kills most reflected and stored XSS for free.
They stop saving you the moment you reach for dangerouslySetInnerHTML, innerHTML or v-html. That's where you look first.
SameSite=Lax cookies (now the browser default) blunt classic cross-site CSRF on top-level navigations.
Anti-CSRF tokens or requiring a custom header on state-changing requests close the gap. Pure token-in-header auth (no cookies) sidesteps CSRF entirely.
Deserialization, path traversal, file handlingtrust placed in bytes from outside
- Insecure deserialization. Feeding attacker bytes to
pickle.loads, JavareadObjector an unsafe YAML loader can run code during reconstruction. Use data-only formats (JSON) andyaml.safe_load; never deserialize untrusted input into live objects. - Path traversal. A filename like
../../etc/passwdescapes the intended directory. Resolve the full path and assert it stays inside the allowed root after normalization - don't just strip.., which a double-encode defeats. - File-handling bugs. Trusting a client-supplied content type or extension, serving uploads from the app origin or unzipping without a size cap (zip bombs). Validate by content, store outside the web root, serve from a sandboxed origin.
When you flag a finding, prioritize by reachability and impact, not by the scanner's red badge. Say it out loud: "This SQLi is on an authenticated admin-only path, so it's real but lower priority than the SSRF on the unauthenticated webhook endpoint, which an anonymous attacker can hit to steal cloud creds." Reasoning about exploitability is the senior signal; reading a severity label is not.
Takeaway. Most web vulns reduce to a few classes; name the root cause (data treated as code, trust placed in client bytes) and the paved-road fix that kills the whole class, then prioritize by reachability, not scanner color.
Self-check
QA reviewer flags four issues. Which should be fixed first and what's the reasoning a senior security engineer gives?
Authentication & authorization
After this you can reason about authn/authz failures and correct designs.
Authentication asks "who are you?" Authorization asks "are you allowed to do this?" Most real-world breaches you'll review aren't login bypasses - they're authorization that someone forgot to write.
- Authentication (authn)
- Proving identity: passwords, OAuth, SSOSingle Sign-On. One company login (usually via SAML or OIDC) instead of a separate password per tool., MFA, session establishment. A failure here = impersonation.
- Authorization (authz)
- Enforcing permission on every request, per object: can THIS user do THIS to THIS resource? A failure here = privilege escalation or data leak.
Authn happens once at the door; authz must happen on every action behind it.
IDOR is the one you'll be shownbroken object-level authorization
Broken Object-Level Authorization (IDOR) tops the OWASP API list for a reason: the endpoint authenticates you fine, then trusts an ID in the request and skips the check that this user owns that object. GET /api/invoices/1043 returns invoice 1043 whether or not it's yours.
# vulnerable - authenticated, but no ownership check invoice = db.get(Invoice, request.params.id) return invoice # fixed - scope the query to the caller invoice = db.get(Invoice, id=request.params.id, owner_id=current_user.id) if invoice is None: raise NotFound() # don't leak existence
Search for any object fetched by a client-supplied ID. For each one, ask: where is the line that confirms the current user may touch this specific row? If the authorization lives in the query's WHERE clause (scoped by tenant/owner) it's hard to forget. If it lives in a separate if after the fetch, it's one early-return away from being skipped.
RBAC vs ABAC and centralizing the decisionmake authz hard to forget
- Model
- RBACRole-Based Access Control. Granting permissions by role rather than configuring each person individually.
- Decision is based on
- The user's role (admin, member, viewer)
- Fits when
- Coarse, stable permission sets; easy to reason about and audit
- Model
- ABAC
- Decision is based on
- Attributes of user + resource + context (owner, tenant, time, IP)
- Fits when
- Fine-grained or per-object rules; multi-tenant ownership checks
| Model | Decision is based on | Fits when |
|---|---|---|
| RBACRole-Based Access Control. Granting permissions by role rather than configuring each person individually. | The user's role (admin, member, viewer) | Coarse, stable permission sets; easy to reason about and audit |
| ABAC | Attributes of user + resource + context (owner, tenant, time, IP) | Fine-grained or per-object rules; multi-tenant ownership checks |
Real systems blend both: RBAC for what features you see, ABAC for which rows you may touch.
The design that survives is one where authorization is a single chokepoint every request flows through - a policy layer, middleware or a can(user, action, resource) helper. Scattered if user.is_admin checks rot: someone adds an endpoint and forgets one. Centralize the decision so the default is deny and forgetting it fails closed.
Sessions, tokens and JWT misusewhere token handling goes wrong
- `alg: none` and algorithm confusion. A verifier that trusts the token's own
algheader can be tricked into skipping signature checks or into verifying an RS256 token with the public key used as an HMAC secret. Pin the expected algorithm server-side. - No expiry or no revocation. Long-lived JWTs can't be recalled once issued. Keep access tokens short and pair them with a server-checked refresh or session, so a stolen token dies quickly.
- Tokens in the wrong place. A JWT in
localStorageis reachable by any XSS. Prefer HttpOnly, Secure, SameSite cookies for browser sessions; keep secrets out of URLs and logs. - Trusting unsigned claims. Authorization decisions must read server-verified claims, never fields the client can edit.
is_admin: truein a tampered token is the classic.
Never trust the client for tenant scoping. The tenant ID must come from the authenticated session, not a request parameter or header the caller sets. The cleanest enforcement pushes tenant scoping into the data layer (row-level security or a query that always filters by tenant_id = session.tenant) so a single forgotten check can't cross-tenant-leak. For Cursor, where customer code and context are the crown jewels, tenant isolation is the breach you most need to make structurally impossible.
Takeaway. Authn proves identity once; authz must be enforced on every request, per object - scope queries by owner/tenant so IDOR and cross-tenant leaks fail closed and centralize the decision so it's hard to forget.
Self-check
Secrets, dependencies & supply chain
After this you can evaluate secret-handling and supply-chain risk in a codebase.
The fastest way into a company isn't a clever exploit - it's a leaked key in a git history or a backdoored dependency you installed yourself. Supply-chain and secrets review is high-impact because the failure mode is total.
Secret managementthe rules and how to check them
- No secrets in code or logs. Keys belong in a secrets manager or injected env, never committed and never printed. Grep history, not just
HEAD- a removed key is still in the pack files. - Scope tightly. A token should grant the least it needs (one bucket, read-only, one environment). A broad key turns one leak into full compromise.
- Rotate and make rotation cheap. Rotation only happens if it's a one-command operation. If rotating a key means an outage, it never gets rotated, so design for it up front.
- Detect. Secret scanning on commit and in CI catches the leak before it ships. Assume a secret that ever touched a repo is burned and must be rotated, even after you delete the line.
Dependency and supply-chain riskyour code is mostly other people's code
- Risk
- Vulnerable dependency
- What it looks like
- A known CVE in a package you pull in directly
- Control
npm audit/pip-audit/ Dependabot; patch on a cadence
- Risk
- Transitive risk
- What it looks like
- The vuln is three levels down, not in your manifest
- Control
- SBOMSoftware Bill of Materials. A list of every component and dependency in a build, like an ingredients label for software. to see the full tree; lockfiles to pin every level
- Risk
- Unpinned versions
- What it looks like
^1.2.0silently pulls a compromised 1.9.0- Control
- Pin exact versions + lockfile + integrity hashes
- Risk
- Build-pipeline tampering
- What it looks like
- Malicious code injected during build, not in source
- Control
- Hermetic, reproducible builds; signed artifacts; provenance
| Risk | What it looks like | Control |
|---|---|---|
| Vulnerable dependency | A known CVE in a package you pull in directly | npm audit / pip-audit / Dependabot; patch on a cadence |
| Transitive risk | The vuln is three levels down, not in your manifest | SBOMSoftware Bill of Materials. A list of every component and dependency in a build, like an ingredients label for software. to see the full tree; lockfiles to pin every level |
| Unpinned versions | ^1.2.0 silently pulls a compromised 1.9.0 | Pin exact versions + lockfile + integrity hashes |
| Build-pipeline tampering | Malicious code injected during build, not in source | Hermetic, reproducible builds; signed artifacts; provenance |
Most of your attack surface is dependencies you never read. SBOM + pinning make it visible and stable.
SolarWinds wasn't a vuln in shipped source - attackers compromised the build pipeline and injected a backdoor into a signed, trusted update that thousands of customers installed. The lesson for review: the build system is part of your trust boundary. A clean codebase that's built on a machine anyone can SSH into, from dependencies pulled at build time over plain HTTP, is not actually trustworthy. Treat CI as production.
CI gates as paved-road controlsmake the secure path the default path
The builder-first move is to encode these checks so engineers never have to remember them. Secret scanning and dependency audit run in CI; a finding blocks the merge. The developer gets a clear message at the moment they can fix it, not a ticket three weeks later.
- run: gitleaks detect --no-banner # block committed secrets - run: pip-audit --strict # block known-vuln deps - run: cosign verify $IMAGE # only signed artifacts deploy
When an AI agent writes code, it can confidently import or npm install a package that doesn't exist - a hallucinated dependency. Attackers watch for common hallucinated names and register them ("slopsquatting"), so the next agent that suggests that package installs malware. This is squarely Cursor's threat model: an agent suggesting dependencies is a supply-chain decision. Defenses are an allowlist of approved packages, resolving against a curated internal registry and a CI gate that fails on any package not seen before a human approves it.
Takeaway. Treat every secret that touched a repo as burned, pin and SBOMSoftware Bill of Materials. A list of every component and dependency in a build, like an ingredients label for software. your dependency tree, put the build pipeline inside your trust boundary and enforce it all as CI gates - and remember agent-suggested packages can be hallucinated and squatted.
Self-check
Secure code review like a pro
After this you can run a structured secure code review under time pressure.
The onsite hands you a few hundred lines of plausible-looking code and a clock. The pros don't read top to bottom - they follow the data, from where untrusted input enters to where it does something dangerous.
Carry one method and narrate it. The reviewers are grading whether you have a repeatable approach that finds real bugs and a habit of pairing each finding with a fix an engineer can apply today.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Entry points → trust boundaries → sinks → controls, then triage and write. The boundary and control steps are the gates where bugs live.
- 1Map the entry points. Where does untrusted data enter - routes, request bodies, query params, headers, file uploads, webhooks, message queues? Start at the perimeter.
- 2Draw the trust boundaries. Mark where data crosses from untrusted to trusted: the auth check, the tenant scoping, the deserializer. Bugs cluster on these lines.
- 3Follow data to the sinks. Trace each input to where it does something powerful: a DB query, a shell call, a file path, an HTML render, an outbound fetch. An input that reaches a sink unsanitized is a finding.
- 4Check the controls. At each boundary and sink, is there authn, authz, validation, encoding or escaping? Note what's missing, not just what's present.
- 5Triage by reachability and impact. Rank what an attacker can actually hit and what they gain. Unauthenticated + high-impact rises to the top.
- 6Write each finding with a fix. State the vuln, the exact line, the impact in one sentence and a concrete remediation. No remediation, no finding.
This is the whole method in four words. It works on code you've never seen and in any language, because it follows the structure of how attacks actually work - untrusted data flowing to a dangerous operation without a control in between. Say the four words at the start of the round so the interviewer sees the method before you find anything.
When you point an agent at a review or a non-trivial change, default to Plan modeA mode that makes no edits: it researches the codebase and produces an editable plan you review before any code changes.. In plan, the agent aggressively farms for context up front - it goes and finds all of the injection points ahead of time before it writes a line. One security engineer described himself as a complete plan convert for exactly this reason: planning makes the model form context more comprehensively than it would by default, and it keeps the work on track during execution. You can still ask questions inside the plan and read what it decided to do, then push back - things like "what if somebody sends a message that says ignore previous instructions and fire me" surface prompt-injection paths the plan should account for. Skip it for quick changes; reach for it on anything net-new or involved.
Write the finding like a PR commentclear, actionable, non-preachy
A finding that lands is one the author can act on without re-deriving your reasoning. Skip the lecture. Name the bug, show the line, state the impact and give the fix - ideally as a suggested diff.
SSRF - line 42, `fetch(req.body.url)` calls an attacker-controlled URL with no allowlist, so it can reach internal services and cloud metadata. Fix: resolve the host, reject private/link-local ranges and allowlist the handful of domains this integration actually needs.
The class named (SSRF, IDOR, XSS) so it's searchable.
The exact location and a one-sentence impact.
A concrete fix, ideally a diff the author can accept.
Moralizing or vague "this isn't secure" with no line.
A theoretical risk no attacker can reach, flagged as critical.
A fix that breaks the feature, with no path that keeps it working.
I'll start at the entry points and follow untrusted data to the dangerous sinks, checking the control at each boundary. I'll call out findings as I go, rank them by what an unauthenticated attacker can reach and give each one a concrete fix.
That framing is exactly how Cursor's own agent-assisted Security Review operates in production: it leaves findings as PR comments and blocks CI on security issues. Reviewing the way the company already reviews is a strong, honest signal that you'd fit the workflow.
Takeaway. Run one method out loud - entry points → trust boundaries → sinks → controls - triage by reachability and write every finding like a PR comment: class, line, impact and a concrete fix.
Self-check
QYou have 30 minutes and 400 lines of unfamiliar code. What's the first thing you do and why not just read top to bottom?
AI-augmented review (the Cursor way)
After this you can explain how agent-assisted security review works and its tradeoffs.
Cursor reviews its own code with AI. The agent-assisted Security Review runs on 3,000+ internal PRs a week and has kept hundreds of issues out of production - the flagship responsibility this role exists to build.
Be honest about the public facts and reason from them. Cursor built a dedicated agent-assisted review rather than buying a general scanner, because an off-the-shelf tool couldn't be tuned to the threat model of a company shipping autonomous coding agents. The general lesson is that a security review is only as good as how well it understands what you're actually afraid of.
A security engineer who ships this workflow puts the agent-vs-traditional-scanner call at 100,000% in favor of agents, and the reason is context: a SASTStatic Application Security Testing. Scanning source code for vulnerabilities without running it. rule can pattern-match a line, but it can't trace where the data came from or where it goes. The viable hybrid is to feed the linter or scanner output into the agent and let it triage - during a real vuln scan you can watch the model choose the right checks on the fly rather than running a fixed rule set.
the problem with any kind of static rules is that they're static and it doesn't really have context. So it would be very hard to write a good static rule that actually traced through all the context in the way an agent can.
The agent buys you context, but it introduces a failure mode a static rule doesn't have: it can make things up. That gives you two problems to defend against. False negatives - it misses a real bug - you blunt by running the review in parallel and priming it with specific things to look for. False positives - it invents a bug that isn't there - you blunt with a validation loop, which is exactly why the pipeline below escalates to a human before it enforces anything.
The pipeline, end to endhow a PR becomes a blocked merge
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Cloud agent + custom security MCP tool, model dedup, human validation, then PR comment and CI block. The Slack escalation and CI gate are the control points.
- 1A PR opens. The change triggers a cloud agent dedicated to security review, separate from the engineer's own Cursor session.
- 2The agent investigates with a custom security MCPModel Context Protocol. A standard that lets an AI agent pull in context from outside the repo, like Jira tickets or internal docs. tool. A serverless tool gives the agent the context and capabilities it needs to reason about this codebase's threat model, not a generic ruleset.
- 3Dedup via model classification. A model groups and de-duplicates findings so the same issue across files doesn't spam the author with noise.
- 4Escalate to humans. Plausible findings go to a Slack channel where the security team validates before anything is enforced - human-in-the-loop, not blind automation.
- 5Comment and gate. Confirmed findings land as a PR comment with the fix and security findings block CI so they can't reach production.
- Dedicated agent, not a bought scanner
- Tunable to Cursor's specific threat model (agents that read/write/execute customer code) where a generic tool can't be.
- Custom security MCP tool
- Gives the agent codebase-aware context and capabilities, serverless so it scales across thousands of PRs.
- Model-based dedup
- Keeps signal high; duplicate noise is what makes engineers ignore a security bot.
- Slack escalation before enforcement
- Human validates plausible findings, so a false positive doesn't block a real fix.
Layer the controls across the lifecyclein-flight validation, then PR-time, then manual
The PR-time pipeline above is the last gate, not the only one. A practitioner who builds this kind of review describes a layered architecture where each control sits at a different point in the dev lifecycle, so a bug has to get past several checks to ship.
Interactive diagram. Tab through its regions; each focused region shows its detail in the panel below.
Hooks + rules validate while the agent works; manual bug-hunting runs in between; Bugbot is the last step at PR time. Each layer catches what the previous one missed.
Why block CI on security, not on every nitwhere the friction is worth it
Blocking the build is the strongest, most annoying control you have, so spend it where the cost of shipping the issue is highest. A security vuln in production can mean a breach; a missing test or a style nit can be fixed in the next PR. Gating on security and merely commenting on quality keeps the gate credible - engineers stop respecting a bot that blocks merges over trivia.
- Finding type
- Confirmed security vuln
- Action
- Block CI
- Why
- Cost of reaching production is a potential breach
- Finding type
- Likely vuln, unvalidated
- Action
- Slack escalation, no block
- Why
- Human confirms before paying the friction cost
- Finding type
- Code-quality nit
- Action
- Comment only
- Why
- Low cost to fix later; blocking erodes trust in the gate
| Finding type | Action | Why |
|---|---|---|
| Confirmed security vuln | Block CI | Cost of reaching production is a potential breach |
| Likely vuln, unvalidated | Slack escalation, no block | Human confirms before paying the friction cost |
| Code-quality nit | Comment only | Low cost to fix later; blocking erodes trust in the gate |
Interviewers want you to name the hard parts, not pitch the magic. False positives erode trust fast, so dedup and human-in-the-loop validation exist precisely to keep the signal worth a developer's attention. The review is only as good as how well its prompts encode the threat model, so prompt-tuning is ongoing security work, not a one-time setup. And an agent reviewing code is itself attackable - a PR could carry a prompt-injection payload aimed at the reviewer agent, which is its own threat to design against.
Tie it back to the builder-first culture. The point of agent-assisted review isn't to replace human reviewers - it's a paved road that engages the right humans on the important changes at the right time and shrinks the class of bugs that reach production. If asked how you'd improve it, reason out loud about measuring precision/recall on real findings, tuning prompts against actual past incidents and defending the reviewer agent itself from injection. Truth-seeking over a polished pitch is the value they're testing.
The offensive edge: from PoC to exploitwhere agent-driven security is heading
The same workflow that reviews defensively also points toward offensive security. A practitioner working in this space frames it honestly: the agent already finds bugs and writes proof-of-concepts, so there is not a giant leap to having it run that PoC as an actual exploit, and teams are known to be chaining A-to-B-to-C attack paths. His own preference is to lean on perfect visibility of the code and the configuration as the source of truth - if he can get a validated issue straight from code and config, he doesn't necessarily need a live exploit to prove it. The discipline worth copying: don't warranty work you haven't actually done yourself.
Takeaway. Cursor's agent-assisted Security Review (cloud agent + custom security MCPModel Context Protocol. A standard that lets an AI agent pull in context from outside the repo, like Jira tickets or internal docs. tool, model dedup, Slack escalation, PR comment + CI block) is a paved road that gates only on confirmed security findings - and its real engineering is dedup, threat-model prompt-tuning and human-in-the-loop validation.
Self-check
QWhy does Cursor block CI on security findings but only leave comments for code-quality issues?