MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Web Security: OWASP Top 10 and Prevention

Protect web apps against OWASP Top 10: injection, XSS, CSRF, and security misconfiguration.

SecurityOWASPWebBackend

By MinhVo

Introduction

The OWASP Top 10 represents the most critical web application security risks as identified by the Open Web Application Security Project. Updated periodically based on real-world vulnerability data, the 2021 edition introduced new categories like Insecure Design and Software and Data Integrity Failures while reshuffling existing risks based on their prevalence and impact. Understanding and mitigating every item on this list is the baseline expectation for any production web application.

OWASP Top 10 security risks

Injection attacks alone accounted for 94% of applications tested in OWASP's dataset. Broken access control overtook injection as the #1 risk in 2021, reflecting the widespread failure to properly enforce authorization on every API endpoint and data access path. These aren't theoretical risks—every item on the OWASP Top 10 has been exploited in real breaches affecting millions of users.

This guide covers each OWASP Top 10 category with specific, actionable prevention strategies, code examples, and architectural patterns that eliminate these vulnerabilities at their root.

Understanding OWASP Top 10: Core Concepts

A01: Broken Access Control

Broken access control occurs when users can act outside their intended permissions. This includes accessing other users' data, modifying records they don't own, escalating privileges, and accessing admin functions without authorization. The 2021 edition elevated this to #1 because it appeared in 94% of tested applications.

A02: Cryptographic Failures

Previously "Sensitive Data Exposure," this category focuses on failures in cryptography that lead to data exposure. Weak algorithms (MD5, SHA-1), hardcoded keys, missing encryption at rest, and improper certificate validation are common manifestations. The shift in naming emphasizes that the root cause is cryptographic failure, not just the symptom of exposed data.

A03: Injection

Injection flaws occur when untrusted data is sent to an interpreter as part of a command or query. SQL injection, NoSQL injection, OS command injection, and LDAP injection all follow the same pattern: user input is concatenated into executable strings without proper sanitization or parameterization.

Security architecture

A04: Insecure Design

New in 2021, insecure design represents architectural flaws that cannot be fixed by implementation alone. Missing rate limiting, lack of fraud detection, insufficient abuse case modeling, and failure to separate tenants at the design level all constitute insecure design.

A05: Security Misconfiguration

Default credentials, unnecessary features enabled, overly permissive CORS, missing security headers, and verbose error messages are common misconfigurations. Cloud infrastructure adds complexity with misconfigured S3 buckets, permissive IAM policies, and exposed management interfaces.

Architecture and Design Patterns

Component 1: Authorization Layer

A centralized authorization layer enforces access control decisions at the API boundary. Every request is evaluated against a policy engine (OPA, Casbin, or custom) that checks subject, resource, action, and context. Authorization logic is never implemented inline within business logic.

Component 2: Input Validation Pipeline

An input validation pipeline sanitizes all external data before it enters the application. Schema validation (Zod, Joi, JSON Schema) enforces type safety and value constraints. Parameterized queries eliminate SQL injection, and output encoding prevents XSS.

Component 3: Cryptographic Service

A cryptographic service centralizes encryption, hashing, and key management. It enforces algorithm standards (AES-256-GCM, bcrypt, Argon2), manages key rotation, and provides audit logging for all cryptographic operations.

Component 4: Security Configuration Management

Infrastructure-as-code (Terraform, Pulumi) and configuration templates ensure consistent security settings across environments. Automated security scanning (Checkov, tfsec) catches misconfigurations before deployment.

Step-by-Step Implementation

Preventing Broken Access Control (A01)

import { z } from 'zod';
 
// Centralized authorization middleware
function authorize(requiredPermission: string) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const user = req.user;
    if (!user) {
      return res.status(401).json({ error: 'Authentication required' });
    }
 
    // Check permission against policy engine
    const allowed = await policyEngine.evaluate({
      subject: user.id,
      action: requiredPermission,
      resource: req.path,
      context: {
        method: req.method,
        params: req.params,
        ip: req.ip,
      },
    });
 
    if (!allowed) {
      await logSecurityEvent({
        type: 'authorization_failure',
        userId: user.id,
        path: req.path,
        permission: requiredPermission,
      });
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
 
    next();
  };
}
 
// Object-level authorization check
async function getUserProfile(requesterId: string, targetUserId: string) {
  // Always verify the requester can access this specific resource
  if (requesterId !== targetUserId) {
    const hasPermission = await policyEngine.evaluate({
      subject: requesterId,
      action: 'read:user_profile',
      resource: `user:${targetUserId}`,
    });
    if (!hasPermission) throw new ForbiddenError();
  }
 
  return db.user.findUnique({ where: { id: targetUserId } });
}

Preventing SQL Injection (A03)

// VULNERABLE: String concatenation
const query = `SELECT * FROM users WHERE email = '${email}'`; // NEVER DO THIS
 
// SECURE: Parameterized queries with Prisma
const user = await prisma.user.findUnique({
  where: { email: email },
});
 
// SECURE: Parameterized queries with raw SQL
const user = await db.query(
  'SELECT * FROM users WHERE email = $1',
  [email]
);
 
// SECURE: Input validation with Zod
const UserQuerySchema = z.object({
  email: z.string().email().max(255),
  id: z.string().uuid().optional(),
});
 
async function getUser(rawInput: unknown) {
  const input = UserQuerySchema.parse(rawInput); // Throws on invalid input
  return db.user.findUnique({ where: { email: input.email } });
}

Preventing XSS (A03/A05)

import DOMPurify from 'isomorphic-dompurify';
 
// Server-side: HTML sanitization
function sanitizeUserHTML(html: string): string {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li'],
    ALLOWED_ATTR: ['href', 'title', 'class'],
    ALLOW_DATA_ATTR: false,
  });
}
 
// Client-side: React auto-escaping (safe by default)
function UserContent({ content }: { content: string }) {
  // React auto-escapes this — safe
  return <div>{content}</div>;
}
 
// Client-side: dangerouslySetInnerHTML with sanitization
function RichContent({ html }: { html: string }) {
  const clean = DOMPurify.sanitize(html);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
 
// Prevent stored XSS: sanitize on input AND output
app.post('/api/comments', async (req, res) => {
  const { body } = CommentSchema.parse(req.body);
  const sanitized = sanitizeUserHTML(body);
  await db.comment.create({ data: { body: sanitized, userId: req.user.id } });
  res.json({ success: true });
});

Preventing CSRF (A01)

import csrf from 'csurf';
 
// CSRF protection middleware
const csrfProtection = csrf({
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
  },
});
 
// Apply to all state-changing routes
app.use('/api', csrfProtection);
 
// Client-side: Include CSRF token in requests
app.get('/csrf-token', csrfProtection, (req, res) => {
  res.json({ token: req.csrfToken() });
});
 
// Frontend: Include token in form submissions and AJAX requests
const csrfToken = await fetch('/csrf-token').then((r) => r.json());
await fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'CSRF-Token': csrfToken.token,
  },
  body: JSON.stringify({ amount: 100, to: 'account2' }),
});

Security Misconfiguration Prevention (A05)

// Security configuration checklist
const securityConfig = {
  // Remove default credentials
  admin: {
    username: process.env.ADMIN_USERNAME, // Never use 'admin'
    password: process.env.ADMIN_PASSWORD, // Never use 'password'
  },
  
  // CORS: Restrict to known origins
  cors: {
    origin: ['https://yourdomain.com', 'https://app.yourdomain.com'],
    credentials: true,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
  },
  
  // Error handling: Don't leak internals
  errorHandler: (err: Error, req: Request, res: Response) => {
    console.error(err); // Log full error server-side
    res.status(500).json({ error: 'Internal server error' }); // Generic response
  },
  
  // Disable unnecessary features
  helmet: {
    contentSecurityPolicy: true,
    crossOriginEmbedderPolicy: true,
    crossOriginOpenerPolicy: true,
    crossOriginResourcePolicy: true,
    hidePoweredBy: true,
    hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
    noSniff: true,
    referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
    xssFilter: false, // Disabled in favor of CSP
  },
};

Security testing pipeline

Real-World Use Cases and Case Studies

Use Case 1: Broken Access Control in SaaS Platform

A SaaS platform discovered that API endpoints returned data across tenant boundaries. The /api/v1/invoices/{id} endpoint accepted any invoice ID without verifying the requesting user's tenant ownership. Fix: implement row-level security in the database layer, enforcing WHERE tenant_id = current_tenant() on every query.

Use Case 2: SQL Injection in Legacy Application

A legacy PHP application used string concatenation for all database queries. An attacker exploited a search parameter to extract the entire user database including password hashes. Remediation: migrated to PDO with prepared statements, added WAF rules as temporary mitigation, and implemented automated SQL injection testing in CI/CD.

Use Case 3: Security Misconfiguration Exposing Admin Panel

A Kubernetes deployment exposed the admin dashboard to the internet with default credentials. An attacker gained cluster-admin access and deployed cryptocurrency miners. Fix: disabled dashboard access outside the VPN, implemented RBAC with least-privilege roles, and added CIS Kubernetes benchmark scanning to the deployment pipeline.

Best Practices for Production

  1. Deny by default: Every access control decision should default to deny. Only explicitly granted permissions allow access. This prevents new endpoints from being accidentally exposed.

  2. Use parameterized queries exclusively: Never concatenate user input into SQL, NoSQL, or OS commands. ORMs like Prisma and TypeORM handle this automatically; for raw queries, always use parameter placeholders.

  3. Validate on input, encode on output: Validate all input with strict schemas (Zod, JSON Schema). Encode all output based on context: HTML encoding for web pages, JavaScript encoding for inline scripts, URL encoding for URLs.

  4. Encrypt sensitive data at rest and in transit: Use AES-256-GCM for encryption, bcrypt/Argon2 for password hashing. Never store encryption keys with encrypted data. Use envelope encryption with a KMS.

  5. Implement security logging and monitoring: Log all authentication events, authorization failures, input validation failures, and configuration changes. Alert on anomalous patterns. Maintain audit trails for compliance.

  6. Keep dependencies updated: Run npm audit in CI/CD pipelines. Subscribe to security advisories for critical dependencies. Use Dependabot or Renovate for automated update PRs.

  7. Conduct threat modeling during design: Use STRIDE or PASTA frameworks to identify threats before implementation. Document trust boundaries, data flows, and attack surfaces. Design mitigations into the architecture.

  8. Deploy WAF as defense-in-depth: A Web Application Firewall (WAF) provides an additional layer of protection against known attack patterns. AWS WAF, Cloudflare WAF, and ModSecurity offer pre-built rulesets for OWASP Top 10.

Common Pitfalls and Solutions

PitfallImpactSolution
IDOR (Insecure Direct Object Reference)Users access other users' dataVerify resource ownership in every endpoint
Client-side-only authorizationEasily bypassed with API toolsAlways enforce authorization server-side
Using MD5/SHA-1 for passwordsPasswords cracked in seconds with rainbow tablesUse bcrypt or Argon2 with proper cost factors
Verbose error messagesInternal architecture exposed to attackersLog detailed errors server-side, return generic messages
Disabled security features in dev left in prodMissing protection in productionEnvironment-aware security configuration
Missing rate limitingBrute force and credential stuffing attacksImplement rate limiting per IP and per user

Performance Optimization

Security controls add measurable overhead. Parameterized queries may require query plan caching. Encryption adds CPU cycles. Authorization checks require database lookups. Optimize by caching authorization decisions (with short TTLs), using connection pooling for database queries, and offloading encryption to hardware acceleration when available.

// Authorization decision caching
const authCache = new LRUCache<string, boolean>({
  max: 10000,
  ttl: 60 * 1000, // 1 minute
});
 
async function checkPermissionCached(
  userId: string,
  permission: string,
  resource: string
): Promise<boolean> {
  const key = `${userId}:${permission}:${resource}`;
  const cached = authCache.get(key);
  if (cached !== undefined) return cached;
 
  const result = await policyEngine.evaluate({ subject: userId, action: permission, resource });
  authCache.set(key, result);
  return result;
}

Comparison with Alternatives

OWASP CategoryManual Code ReviewSAST ToolsDAST ToolsRASP
Broken Access ControlMedium detectionLowMediumHigh
InjectionHigh detectionHighHighHigh
XSSMedium detectionHighHighHigh
Insecure DesignHigh (requires expertise)LowLowN/A
MisconfigurationLowMediumMediumN/A
SpeedVery slowFastMediumReal-time
False Positive RateLowHighMediumLow

A comprehensive security program combines all four approaches: SAST in CI/CD, DAST against staging, RASP in production, and periodic manual code review for architectural flaws.

Advanced Patterns and Techniques

Row-Level Security in PostgreSQL

-- Enable RLS on the users table
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
 
-- Users can only see their own data
CREATE POLICY user_isolation ON users
  FOR ALL
  USING (tenant_id = current_setting('app.current_tenant')::uuid);
 
-- Set tenant context per request
CREATE FUNCTION set_tenant(tenant_id uuid) RETURNS void AS $$
BEGIN
  PERFORM set_config('app.current_tenant', tenant_id::text, true);
END;
$$ LANGUAGE plpgsql;

Input Validation Pipeline

import { z } from 'zod';
import { createMiddleware } from 'hono/factory';
 
function validateInput<T extends z.ZodSchema>(schema: T) {
  return createMiddleware(async (c, next) => {
    const result = schema.safeParse(await c.req.json());
    if (!result.success) {
      return c.json({ error: 'Validation failed', details: result.error.issues }, 400);
    }
    c.set('validatedInput', result.data);
    await next();
  });
}
 
// Usage
const CreateUserSchema = z.object({
  email: z.string().email().max(255),
  name: z.string().min(1).max(100).regex(/^[a-zA-Z\s]+$/),
  role: z.enum(['user', 'admin']).default('user'),
});
 
app.post('/api/users', validateInput(CreateUserSchema), async (c) => {
  const input = c.get('validatedInput');
  // Input is guaranteed valid and typed
  const user = await createUser(input);
  return c.json(user);
});

Testing Strategies

OWASP Top 10 vulnerabilities should be tested at every level. Unit tests verify input validation and authorization logic. Integration tests verify parameterized queries and output encoding. End-to-end tests verify that security headers are present, CSRF protection works, and access control is enforced across the full request lifecycle.

import { test, expect } from '@playwright/test';
 
test('prevents SQL injection in search', async ({ request }) => {
  const response = await request.get('/api/search?q=1%27%20OR%201%3D1--');
  expect(response.status()).toBe(400);
  const body = await response.json();
  expect(body.error).toContain('Invalid input');
});
 
test('prevents unauthorized access to other users data', async ({ request }) => {
  // Login as user A
  const loginResponse = await request.post('/api/login', {
    data: { email: 'usera@test.com', password: 'password' },
  });
  const { token } = await loginResponse.json();
 
  // Try to access user B's profile
  const response = await request.get('/api/users/user-b-id/profile', {
    headers: { Authorization: `Bearer ${token}` },
  });
  expect(response.status()).toBe(403);
});

Future Outlook

The OWASP Top 10 continues evolving with the threat landscape. AI-specific vulnerabilities (prompt injection, model poisoning) may appear in future editions. Supply chain security, API security, and LLM security are emerging categories that OWASP is actively researching through dedicated projects.

OWASP Top 10 Testing Checklist

Test each OWASP Top 10 category systematically. For injection vulnerabilities, use parameterized queries and test all user inputs with SQL injection payloads. For broken authentication, test session management, password policies, and multi-factor authentication. For XSS, test all user-generated content rendering with script injection payloads. Use automated scanners like OWASP ZAP or Burp Suite for initial testing, then follow up with manual testing for business logic vulnerabilities that automated tools cannot detect.

Security Headers Configuration

Implement security headers as a defense-in-depth strategy alongside OWASP protections. Set X-Content-Type-Options: nosniff to prevent MIME type sniffing. Configure X-Frame-Options: DENY or use CSP frame-ancestors to prevent clickjacking. Add Strict-Transport-Security with a long max-age to enforce HTTPS. Set Referrer-Policy: strict-origin-when-cross-origin to control information leakage. Use Permissions-Policy to disable unnecessary browser features like camera, microphone, and geolocation for your application.

Conclusion

The OWASP Top 10 provides a prioritized framework for addressing the most critical web application security risks. Prevention requires architectural commitment: centralized authorization, parameterized queries, input validation pipelines, cryptographic best practices, and continuous security testing. Treat the OWASP Top 10 as a minimum security baseline, not a comprehensive security program—real-world applications require additional controls for business logic abuse, API security, and emerging AI-specific threats. Start with broken access control and injection prevention, as these two categories alone account for the majority of real-world breaches.