Software Engineering·July 21, 2026·3 min read

Web Application Security

The most critical web application attack classes — SQL injection, XSS, broken access control, CSRF, and more — explained plainly with concrete defenses for each.

securityowaspweb-securityappsecengineering

Web applications face a predictable set of attack patterns. The OWASP Top 10 is the industry-standard list of the most critical risks, compiled from real-world breach data. This guide explains the most common ones and the concrete defenses for each.

The good news: a modern framework plus a managed auth provider eliminates most of this list by default. Your job is mainly to not undo those protections.

If Cartara flagged this in your diff, you likely added authentication, user input handling, database queries, or API endpoints that touch security-sensitive patterns.


Broken Access Control

The most common and most damaging class. The app authenticates users correctly but fails to verify what each user is allowed to do.

The classic form is IDOR (Insecure Direct Object Reference): changing /orders/123 to /orders/124 in the URL to see someone else's order.

Defense: enforce every authorization check server-side on every request. Never trust the client to restrict access. Verify ownership before returning or modifying any record.

// ❌ Missing ownership check
const order = await db.orders.findById(req.params.id);
res.json(order); // could return any user's order

// ✅ Verify the order belongs to the current user
const order = await db.orders.findById(req.params.id);
if (order.userId !== req.user.id) {
  return res.status(403).json({ error: 'Forbidden' });
}
res.json(order);

See Authentication and Authorization for depth on this pattern.


SQL Injection

Building a database query by concatenating user input lets an attacker inject SQL — bypassing logic, accessing other users' data, or worse.

Defense: never build queries by string concatenation. Use parameterized queries (your ORM does this by default):

// ❌ Vulnerable to SQL injection
const query = `SELECT * FROM users WHERE email = '${userInput}'`;

// ✅ Safe — user input is treated as data, not code
const user = await db.query('SELECT * FROM users WHERE email = $1', [userInput]);

ORMs like Prisma and Drizzle parameterize queries automatically. This single habit eliminates almost all SQL injection risk.


Cross-Site Scripting (XSS)

An attacker gets malicious JavaScript to run in another user's browser — for example by posting a comment containing <script> code that steals session tokens from anyone who views it.

Defenses:

  • Escape all user-generated content on output (React does this by default with {variable} syntax)
  • Avoid dangerouslySetInnerHTML and innerHTML with untrusted data
  • Add a Content-Security-Policy header to limit what scripts can run
// ❌ Dangerous — executes any HTML/JS in userContent
<div dangerouslySetInnerHTML={{ __html: userContent }} />

// ✅ Safe — React escapes the content
<div>{userContent}</div>

Cross-Site Request Forgery (CSRF)

CSRF tricks a logged-in user's browser into making an unwanted authenticated request — for example, a hidden form on a malicious site that submits a "change email" action to your app using the user's existing session cookie.

Defenses:

  • Set cookies with SameSite=Strict or SameSite=Lax (stops cookies being sent on cross-site requests)
  • Use anti-CSRF tokens for state-changing forms
  • Require re-authentication for sensitive actions like changing a password or email

Most auth providers and frameworks handle CSRF protection automatically — make sure it's enabled and not disabled.


Security Misconfiguration

Not a clever attack, just a door left open:

  • Debug mode or verbose stack traces left on in production (leaks internal details)
  • Default credentials never changed
  • Unnecessary services exposed to the internet
  • Overly permissive CORS settings that allow any origin

Defense: disable debug output in production, use environment-specific configuration, and expose only what's necessary.


Cryptographic Failures

Sending or storing sensitive data without proper protection:

  • Serving pages over plain HTTP
  • Storing passwords as plain text or with fast hashing (MD5, SHA1)
  • Encrypting data with weak algorithms or rolling your own crypto

Defenses:

  • HTTPS everywhere (every modern hosting platform provides this automatically)
  • Hash passwords with bcrypt or Argon2 (your auth provider does this for you)
  • Use vetted libraries — never invent your own cryptography

Security Headers

A cheap, high-value layer that limits the damage of other vulnerabilities:

// Using the 'helmet' library in Express/Fastify
import helmet from 'helmet';
app.use(helmet());

// Or set headers manually
res.setHeader('Content-Security-Policy', "default-src 'self'");
res.setHeader('Strict-Transport-Security', 'max-age=31536000');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');

Most platforms and the helmet library set these in one step.


What You'll See in Your Code

Security-related code typically shows up in diffs as:

  • Ownership checks before returning or modifying data
  • Input sanitization or validation before storing user-submitted content
  • Parameterized queries instead of string concatenation
  • Security middleware (helmet, CORS config, CSRF middleware)
  • Auth middleware protecting routes

Related concepts

Authentication and Authorization
Authentication verifies who you are. Authorization decides what you're allowed to do. This guide explains both, how sessions and tokens work, and why you should use an auth provider instead of building your own.
Rate Limiting and Throttling
Rate limiting caps how many requests a client can make in a given window. It protects against abuse, runaway costs from paid APIs, and accidental self-inflicted overload.
Secrets and Config Management
Every app has settings that change between environments and sensitive values that must never leak. This guide explains the difference between config and secrets, where each should live, and how to manage rotation.

Turn shipping into understanding

Cartara measures what your team actually learns from every AI coding session.

Join the waitlist