Security basics every SWE is expected to know
📖 Walk me through it — plain English
This lesson is a checklist of the most common ways web apps get hacked, plus the one-line fix for each. You will not be asked to invent cryptography. In an interview the real question is "look at this code — what's the security bug?" and the win is just naming the category and the standard fix. "OWASP" is simply the name of a well-known nonprofit that publishes the list of these top web risks, so "OWASP greatest hits" means "the bugs everyone is expected to recognize."
Here is the everyday analogy. Think of your app as a building. Each risk on this card is a different way a burglar gets in, and each fix is the matching lock. XSS (cross-site scripting) is letting a stranger slip their own note into your bulletin board, and your app reads the note aloud as if it were a command — the fix is to treat every visitor-supplied text as plain words to display, never as instructions to run ("escape on output"). SQL injection is the same trick aimed at your database: if you paste a user's typing directly into a database command, they can append "...and also delete everything"; the fix is a "parameterized query," meaning you hand the database the command and the user's value in two separate slots so the value can never be read as part of the command. CSRF (cross-site request forgery) is a forged work order — a malicious website tricks your already-logged-in browser into firing off a real request (like "transfer money") because the browser automatically attaches your login cookie; the fix is a secret token the attacker can't guess, plus marking cookies "SameSite" so they don't tag along on requests coming from other sites.
The next few are about identity and secrets. Passwords must never be stored as-is; you store a "hash," a scrambled fingerprint that can't be reversed. Use bcrypt or argon2 (purpose-built, deliberately slow algorithms) with a per-user "salt" (random text mixed in so two people with the same password get different fingerprints) — plain SHA-256 is wrong because it has no salt and is too fast to compute, which makes guessing easy. Sessions vs JWT is how you remember a user is logged in: a session keeps the record on your server (so you can instantly revoke it — like a hotel that can deactivate a keycard), while a JWT is a self-contained signed token the user carries (lighter, but hard to cancel before it expires). Secrets in code means never committing API keys or passwords into your repository; put them in environment variables or a secret manager and scan commits to catch leaks. HTTPS + HSTS just means encrypt all traffic, and the HSTS header tells browsers "never downgrade me to unencrypted."
The sneakiest one is IDOR (insecure direct object reference). A URL like /api/orders/123 trusts the number in the address bar. If your code only checks "is this person logged in?" but not "does this order actually belong to them?", a logged-in user can change 123 to 124 and read someone else's order. The fix is an ownership check, not just a login check. The deeper lesson: never trust any value the client hands you — IDs, prices, quantities — because the user can edit all of it.
How to approach the "what's wrong with this code?" question — scan in this order:
- Is user input glued straight into a SQL query, an HTML page, or a shell command? (injection / XSS)
- Does it fetch an object by a client-supplied ID without checking ownership? (IDOR)
- Are passwords stored plainly or with a fast unsalted hash? (auth)
- Are there hard-coded API keys or passwords sitting in the source? (secrets)
- Does it trust client-supplied amounts, prices, or roles? (broken trust)
Why this is enough: you score points by pattern-matching the smell and naming the standard defense (escape output, parameterized query, CSRF token + SameSite, bcrypt + salt, ownership check, secret manager, TLS). Memorizing the category-to-fix mapping above covers the vast majority of what general SWE interviews probe.
You won't be asked to break crypto — you'll be asked "what's wrong with this code?" or "how would you store passwords?" The bar is recognizing the OWASP greatest hits and naming the fix.
The one idea behind all of it: never trust client input
Almost every vulnerability below is the same mistake wearing a different costume: your server believed something the user controls. The user controls the text they type, the URL they visit, the numbers in a form, the cookies and headers their browser sends, even the request body — all of it can be hand-edited or replayed by a tool like curl. So the rule of thumb that survives every interview is: treat all input from the outside world as hostile until you have validated it on the server. Client-side checks (the red box that appears when you type a bad email) are for user convenience only; an attacker simply skips your JavaScript and talks to your API directly, so every check that matters must be re-done on the server.
The second idea is defense in depth: don't rely on a single wall. Layer the locks — validate input, and use parameterized queries, and escape output, and check ownership — so that one mistake doesn't hand over the whole building. A few terms recur throughout, so here they are in one place:
- The CIA triad — the three goals security tries to protect: Confidentiality (only authorized people can read the data), Integrity (data can't be tampered with undetected), and Availability (the system stays up and usable). Every risk below threatens at least one of these.
- Authentication vs authorization — authentication ("authn") proves who you are (logging in). Authorization ("authz") decides what you're allowed to do (can this user read order #124?). They are different checks; passing one does not pass the other. IDOR is precisely the bug of doing authn but forgetting authz.
- Hashing vs encryption — hashing is one-way: it turns input into a fixed fingerprint you can't reverse (used for passwords — you only ever compare fingerprints). Encryption is two-way: it scrambles data with a key so it can be unscrambled later with that key (used for data you must read back, like traffic on the wire). Storing passwords encrypted is a mistake — anyone with the key gets every password; hash them instead.
- Salt — random per-user text mixed into a password before hashing, so identical passwords produce different hashes. It defeats precomputed "rainbow table" lookups and stops an attacker from cracking many accounts at once.
- TLS / HTTPS — TLS (Transport Layer Security, the successor to SSL) is the encryption layer; HTTPS is just HTTP running inside TLS. It gives you confidentiality (eavesdroppers see gibberish) and integrity (tampering is detected) for data in transit between browser and server.
- Same-origin policy (SOP) — a browser rule that scripts from one origin (scheme + host + port, e.g.
https://bank.com) generally cannot read responses from a different origin. It's the fence that stopsevil.com's JavaScript from quietly reading your bank's pages. CORS is the controlled way to poke holes in it on purpose. - Principle of least privilege — give every user, service, and credential the minimum access needed for its job, and nothing more. A web app's database account that only needs to read products shouldn't also be able to drop tables. Smaller blast radius when something leaks.
- Secrets — any value that grants access and must stay hidden: API keys, database passwords, signing keys, tokens. The rule: secrets live in environment variables or a secret manager, never in source code, logs, or URLs.
- OWASP — the Open Worldwide Application Security Project, the nonprofit whose "Top 10" is the industry's canonical list of common web risks. When an interviewer says "OWASP Top 10," they mean roughly the categories on this card.
Untrusted input rendered as HTML/JS. Fix: escape on output, use framework auto-escape, set a strict CSP. Never dangerouslySetInnerHTML on user input.
Attacker site triggers a state-changing request using the victim's cookie. Fix: SameSite cookies + CSRF token on mutating routes. GET should never mutate.
String-concatenating user input into queries. Fix: parameterized queries / prepared statements. ORMs do this by default — don't use raw query() with template strings.
Never store plaintext. Hash with bcrypt/argon2 + per-user salt + cost factor. Don't roll your own. SHA-256 of password is wrong (no salt, too fast).
Sessions = server-side store, revocable, slightly heavier. JWTs = stateless, can't revoke easily, watch alg=none + key confusion bugs. Most apps want sessions.
API keys in repos = breach. Use env vars + a secret manager (Vault, AWS Secrets, Doppler). Rotate on leak. Scan with gitleaks pre-commit.
/api/orders/123 returns ANY order if you skip the auth check. Always check ownership, not just authentication.
TLS everywhere. HSTS header prevents downgrade. Mixed content (HTTP asset on HTTPS page) leaks.
SQL injection: the snippet you must recognize
SQL injection happens when user input is glued (concatenated) into a SQL string so the database parses part of that input as commands rather than data. The classic giveaway is a template string or + building a query. Watch what an attacker can type into the email field of the vulnerable version below:
# VULNERABLE — user input concatenated into the SQL text
email = request.get("email")
query = "SELECT * FROM users WHERE email = '" + email + "'"
db.execute(query)
# If email = anything' OR '1'='1 the query becomes
# SELECT * FROM users WHERE email = 'anything' OR '1'='1'
# '1'='1' is always true, so it returns EVERY user. Login bypassed.
The fix is a parameterized query (also called a prepared statement): you write the SQL with placeholders and pass the values separately, so the database treats them strictly as data and never re-parses them as SQL — no amount of quotes or OR tricks can escape the slot.
# FIXED — SQL and value travel in separate slots
email = request.get("email")
db.execute("SELECT * FROM users WHERE email = ?", (email,))
# The ? is a placeholder; email is sent as a pure value.
# "anything' OR '1'='1" is now just a (failed) literal email lookup.
The same lesson generalizes: any time untrusted input flows into a command interpreter — a shell, an OS path, an LDAP filter, a NoSQL query — you have an injection risk, and the cure is the same shape: keep code and data in separate channels rather than stitching them into one string.
XSS: escape on output
Cross-site scripting (XSS) is injection aimed at the browser: untrusted text gets written into a page without being neutralized, so the browser runs it as HTML/JavaScript. The defense is to escape on output — convert dangerous characters like < and > into harmless display entities (<, >) at the moment you render, so the text shows up as literal characters instead of being interpreted as tags.
// VULNERABLE — raw user text dropped into the DOM as HTML
comment = getUserComment()
element.innerHTML = comment
// If comment = <script>steal(document.cookie)</script>
// the browser EXECUTES it — the attacker's script now runs on your page.
// FIXED — render as text, or escape before inserting
element.textContent = comment // browser shows it literally, never runs it
// or escape: < becomes <, > becomes >, so <script> renders as visible text
Modern frameworks (React, Vue, Angular) auto-escape interpolated values for you — which is exactly why reaching for an escape hatch like React's dangerouslySetInnerHTML on user input is dangerous: it turns the auto-escaping off. A strong second layer is a Content Security Policy (CSP), an HTTP header that tells the browser which script sources are allowed to run, so injected inline scripts are blocked even if one slips through.
Common pitfalls
These are the traps that look safe and aren't — each is a frequent interview "gotcha":
- Validating only on the client. A grayed-out button or a JS check stops nobody; attackers call your API directly. Re-validate every rule on the server.
- "Sanitizing" by hand with blacklists. Stripping
<script>or escaping quotes yourself always misses an encoding or edge case. Use parameterized queries and framework escaping, not homemade filters. - Checking authentication but not authorization. "Logged in" is not "allowed." Verify ownership/role on every object access — this is the IDOR trap.
- Trusting client-supplied amounts or roles. Never accept the price, quantity, discount, or
isAdminflag from the request body. Look those up on the server from a trusted source. - Encrypting passwords instead of hashing them. Encryption is reversible; a leaked key exposes every password. Hash with bcrypt/argon2 + salt instead.
- Fast or unsalted password hashes. SHA-256/MD5 are built to be fast, which helps attackers crack billions per second. Slow, salted, purpose-built hashes are the point.
- State-changing GET requests. If
GET /delete?id=5mutates data, a hidden image tag on another site can trigger it (CSRF). Mutations belong on POST/PUT/DELETE with a CSRF token. - Secrets in the repo, logs, or URLs. An API key committed once lives in git history forever, even after you "delete" it. Rotate it and move it to a secret manager.
- Leaking detail in error messages. "User exists but wrong password" tells an attacker which accounts are real. Return generic auth failures.
- Over-broad permissions. A service key with full admin rights "to make it work" violates least privilege and turns any leak into a catastrophe.
Takeaway: nearly every web vulnerability reduces to "the server trusted something the user controls." Keep code and data in separate channels (parameterized queries, escape on output), separate authentication from authorization (check ownership, not just login), hash passwords (bcrypt/argon2 + salt) rather than encrypt them, keep secrets out of source, encrypt traffic with TLS/HTTPS, and apply least privilege everywhere. Layer these — defense in depth — so one slip doesn't open the whole building.
Go deeper (optional):
The canonical reference is the OWASP Top 10, with the practical OWASP Cheat Sheet Series giving copy-paste defenses per topic. These are the only external links you need; everything required for an interview is on this card.