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
dangerouslySetInnerHTMLandinnerHTMLwith untrusted data - Add a
Content-Security-Policyheader 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=StrictorSameSite=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