Skip to main content
Back to Blog
Guide
2026-05-18

Security Testing Complete Guide: OWASP, Tools, and Automation in 2026

Comprehensive security testing guide covering OWASP Top 10, OWASP API Top 10, SAST, DAST, IAST tools like ZAP, Burp Suite, Snyk, Semgrep, CodeQL, SQL injection, XSS, CSRF testing, and DevSecOps automation for 2026.

Security testing is the practice of finding vulnerabilities in software before attackers do. In 2026, with API-first architectures, AI-generated code at unprecedented scale, and increasingly sophisticated supply chain attacks, security testing is no longer the exclusive domain of specialized penetration testers. Every QA engineer, SDET, and developer needs to understand security testing fundamentals and integrate automated security checks into their daily workflow. This guide covers the OWASP Top 10, API-specific security risks, the full spectrum of testing approaches (SAST, DAST, IAST), the tools that matter, and how to build a DevSecOps pipeline that catches vulnerabilities before they reach production.

Key Takeaways

  • The OWASP Top 10 (2021) and OWASP API Security Top 10 are the baseline threat models every team should test against
  • SAST (static analysis) catches vulnerabilities in source code without running the application -- tools like Semgrep, CodeQL, and Snyk Code
  • DAST (dynamic analysis) tests running applications by sending malicious inputs -- tools like OWASP ZAP and Burp Suite
  • IAST (interactive analysis) instruments the application runtime to detect vulnerabilities during normal testing -- combining the strengths of SAST and DAST
  • Automated security testing in CI/CD catches 70-80% of common vulnerabilities without manual intervention
  • Supply chain security (dependency scanning) is critical as 80% of application code comes from open-source dependencies
  • AI-generated code requires additional security scrutiny as LLMs can introduce subtle vulnerabilities

The OWASP Top 10 (2021 Edition)

The OWASP Top 10 is the authoritative list of the most critical web application security risks. Every security testing effort should start here.

A01: Broken Access Control

Access control enforces that users cannot act outside their intended permissions. Broken access control is the number one web application security risk, moving up from fifth place in the 2017 edition.

Common vulnerabilities:

  • Insecure Direct Object References (IDOR): changing a URL parameter to access another user's data
  • Missing function-level access checks: admin endpoints accessible to regular users
  • CORS misconfiguration allowing unauthorized cross-origin access
  • Metadata manipulation: modifying JWT tokens, cookies, or hidden fields to elevate privileges

How to test:

// Testing for IDOR vulnerability
import { test, expect } from '@playwright/test';

test.describe('Access Control - IDOR', () => {
  test('user cannot access another user profile', async ({ request }) => {
    // Login as user A
    const loginRes = await request.post('/api/auth/login', {
      data: { email: 'userA@test.com', password: 'password123' },
    });
    const { token } = await loginRes.json();

    // Try to access user B's data with user A's token
    const response = await request.get('/api/users/user-b-id/profile', {
      headers: { Authorization: `Bearer ${token}` },
    });

    // Should return 403 Forbidden, NOT 200 with user B's data
    expect(response.status()).toBe(403);
  });

  test('regular user cannot access admin endpoints', async ({ request }) => {
    const loginRes = await request.post('/api/auth/login', {
      data: { email: 'regular@test.com', password: 'password123' },
    });
    const { token } = await loginRes.json();

    const adminEndpoints = [
      '/api/admin/users',
      '/api/admin/settings',
      '/api/admin/audit-logs',
      '/api/admin/exports',
    ];

    for (const endpoint of adminEndpoints) {
      const response = await request.get(endpoint, {
        headers: { Authorization: `Bearer ${token}` },
      });
      expect(response.status(), `${endpoint} should be forbidden`).toBe(403);
    }
  });
});

A02: Cryptographic Failures

Previously called "Sensitive Data Exposure," this category covers failures related to cryptography that lead to exposure of sensitive data.

What to test:

  • Are passwords hashed with bcrypt/scrypt/argon2, not MD5/SHA1?
  • Is data in transit encrypted with TLS 1.2+?
  • Are sensitive fields (SSN, credit card numbers) encrypted at rest?
  • Are cryptographic keys rotated and stored securely?
test('API enforces TLS', async ({ request }) => {
  // Attempt HTTP (non-TLS) connection
  try {
    await request.get('http://api.example.com/health');
    // If we reach here, HTTP is allowed -- vulnerability
    expect(true, 'HTTP should redirect to HTTPS or refuse connection').toBe(false);
  } catch (error) {
    // Connection refused or redirect -- expected
  }
});

test('sensitive data is not in URL parameters', async ({ request }) => {
  // Login endpoint should use POST body, not URL params
  const response = await request.post('/api/auth/login', {
    data: { email: 'test@test.com', password: 'password123' },
  });

  // Verify token is in response body, not URL
  const url = response.url();
  expect(url).not.toContain('token=');
  expect(url).not.toContain('password=');
});

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 remain prevalent despite decades of awareness.

Vulnerable code example:

// VULNERABLE: SQL injection
app.get('/users', async (req, res) => {
  const { search } = req.query;
  // Direct string concatenation -- attacker can inject SQL
  const users = await db.query(`SELECT * FROM users WHERE name LIKE '%${search}%'`);
  res.json(users);
});

Fixed code:

// SECURE: Parameterized query
app.get('/users', async (req, res) => {
  const { search } = req.query;
  const users = await db.query('SELECT * FROM users WHERE name LIKE $1', [`%${search}%`]);
  res.json(users);
});

How to test for SQL injection:

test.describe('SQL Injection Prevention', () => {
  const sqlPayloads = [
    "' OR '1'='1",
    "'; DROP TABLE users; --",
    "' UNION SELECT * FROM admin_users --",
    "1; SELECT * FROM information_schema.tables",
    "' OR 1=1 --",
    "admin'--",
    "1' ORDER BY 1--+",
    "' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT version())))--",
  ];

  for (const payload of sqlPayloads) {
    test(`blocks SQL injection: ${payload.substring(0, 30)}`, async ({ request }) => {
      const response = await request.get(`/api/users?search=${encodeURIComponent(payload)}`);

      // Should not return 200 with data -- either 400 (validation) or empty results
      if (response.status() === 200) {
        const body = await response.json();
        // If 200, verify no data leakage
        expect(body.data?.length || 0, 'SQL injection returned unexpected data').toBeLessThan(100);
      }
      // Should never return 500 (unhandled SQL error exposes internals)
      expect(response.status()).not.toBe(500);
    });
  }
});

A04: Insecure Design

Insecure design refers to missing or ineffective security controls at the architecture level. No amount of implementation-level fixes can address a fundamentally insecure design.

Examples:

  • No rate limiting on authentication endpoints (allows brute force attacks)
  • No account lockout after failed login attempts
  • Password recovery that reveals whether an email exists in the system
  • Missing CAPTCHA on sensitive forms
test('rate limiting on login endpoint', async ({ request }) => {
  const attempts = [];
  for (let i = 0; i < 20; i++) {
    const res = await request.post('/api/auth/login', {
      data: { email: 'test@test.com', password: `wrong-${i}` },
    });
    attempts.push(res.status());
  }

  const rateLimited = attempts.filter((s) => s === 429);
  expect(rateLimited.length, 'Login should be rate limited after multiple failures').toBeGreaterThan(0);
});

A05: Security Misconfiguration

This is the most commonly seen issue. It includes unpatched software, default credentials, verbose error messages, unnecessary features enabled, and missing security headers.

test.describe('Security Headers', () => {
  test('response includes required security headers', async ({ request }) => {
    const response = await request.get('/');
    const headers = response.headers();

    // Content-Security-Policy prevents XSS and data injection
    expect(headers['content-security-policy']).toBeDefined();

    // X-Content-Type-Options prevents MIME sniffing
    expect(headers['x-content-type-options']).toBe('nosniff');

    // X-Frame-Options prevents clickjacking
    expect(headers['x-frame-options']).toMatch(/DENY|SAMEORIGIN/);

    // Strict-Transport-Security enforces HTTPS
    expect(headers['strict-transport-security']).toBeDefined();

    // Referrer-Policy controls information leakage
    expect(headers['referrer-policy']).toBeDefined();

    // X-XSS-Protection (legacy but still useful)
    expect(headers['x-xss-protection']).toBeDefined();

    // Server header should not reveal technology stack
    expect(headers['server']).not.toMatch(/Apache|nginx|Express/i);

    // X-Powered-By should be absent
    expect(headers['x-powered-by']).toBeUndefined();
  });
});

A06: Vulnerable and Outdated Components

Using components with known vulnerabilities is one of the easiest attack vectors. Modern applications depend on hundreds of open-source packages, each a potential entry point.

Automated dependency scanning:

# Snyk: comprehensive dependency vulnerability scanning
npx snyk test --severity-threshold=high

# npm audit: built-in Node.js dependency checking
npm audit --audit-level=high

# OWASP Dependency-Check: language-agnostic
dependency-check --project "MyApp" --scan . --format HTML --out reports/

A07: Identification and Authentication Failures

Testing authentication bypass:

test.describe('Authentication Security', () => {
  test('expired JWT is rejected', async ({ request }) => {
    // JWT that expired 1 hour ago
    const expiredToken = createJwt({ exp: Math.floor(Date.now() / 1000) - 3600 });
    const response = await request.get('/api/profile', {
      headers: { Authorization: `Bearer ${expiredToken}` },
    });
    expect(response.status()).toBe(401);
  });

  test('tampered JWT is rejected', async ({ request }) => {
    const validToken = await getValidToken();
    // Modify the payload without re-signing
    const parts = validToken.split('.');
    const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
    payload.role = 'admin';
    parts[1] = Buffer.from(JSON.stringify(payload)).toString('base64url');
    const tamperedToken = parts.join('.');

    const response = await request.get('/api/admin/users', {
      headers: { Authorization: `Bearer ${tamperedToken}` },
    });
    expect(response.status()).toBe(401);
  });

  test('JWT with none algorithm is rejected', async ({ request }) => {
    // "alg: none" attack
    const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url');
    const payload = Buffer.from(JSON.stringify({ sub: '1', role: 'admin' })).toString('base64url');
    const noneToken = `${header}.${payload}.`;

    const response = await request.get('/api/profile', {
      headers: { Authorization: `Bearer ${noneToken}` },
    });
    expect(response.status()).toBe(401);
  });
});

A08: Software and Data Integrity Failures

This includes insecure CI/CD pipelines, auto-update mechanisms without integrity verification, and insecure deserialization.

A09: Security Logging and Monitoring Failures

Without proper logging, attacks go undetected. Test that security-relevant events are logged:

test('failed login attempts are logged', async ({ request }) => {
  await request.post('/api/auth/login', {
    data: { email: 'admin@test.com', password: 'wrong' },
  });

  // Verify via admin API or log inspection
  const logs = await request.get('/api/admin/audit-logs?action=login_failed&limit=1', {
    headers: { Authorization: `Bearer ${adminToken}` },
  });
  const body = await logs.json();
  expect(body.data.length).toBeGreaterThan(0);
  expect(body.data[0].action).toBe('login_failed');
});

A10: Server-Side Request Forgery (SSRF)

SSRF occurs when an application fetches a remote resource without validating the user-supplied URL, allowing attackers to access internal services.

test.describe('SSRF Prevention', () => {
  const ssrfPayloads = [
    'http://127.0.0.1/admin',
    'http://localhost:3000/internal',
    'http://169.254.169.254/latest/meta-data/',  // AWS metadata
    'http://[::1]/admin',
    'http://0x7f000001/admin',
    'file:///etc/passwd',
  ];

  for (const url of ssrfPayloads) {
    test(`blocks SSRF attempt: ${url.substring(0, 40)}`, async ({ request }) => {
      const response = await request.post('/api/fetch-url', {
        data: { url },
      });
      expect(response.status()).not.toBe(200);
    });
  }
});

OWASP API Security Top 10

APIs have their own unique security challenges. The OWASP API Security Top 10 addresses risks specific to API architectures.

API1: Broken Object Level Authorization (BOLA)

The API equivalent of IDOR. Tests should verify that every API endpoint enforcing object-level access checks:

test('BOLA: user cannot access other user orders', async ({ request }) => {
  // Create order as user A
  const orderRes = await request.post('/api/orders', {
    data: { product: 'widget', quantity: 1 },
    headers: { Authorization: `Bearer ${userAToken}` },
  });
  const order = await orderRes.json();

  // Try to access with user B's token
  const response = await request.get(`/api/orders/${order.id}`, {
    headers: { Authorization: `Bearer ${userBToken}` },
  });
  expect(response.status()).toBe(403);
});

API2: Broken Authentication

API authentication issues include weak token generation, missing token validation, and tokens that never expire.

API3: Broken Object Property Level Authorization

APIs may expose object properties that the user should not be able to read or modify:

test('user cannot set admin role via API', async ({ request }) => {
  const response = await request.patch('/api/users/me', {
    data: { name: 'Updated', role: 'admin' },
    headers: { Authorization: `Bearer ${userToken}` },
  });

  const user = await response.json();
  expect(user.role).not.toBe('admin');
});

API4: Unrestricted Resource Consumption

No rate limiting, no pagination limits, no query complexity limits:

test('API enforces pagination limits', async ({ request }) => {
  const response = await request.get('/api/users?limit=999999', {
    headers: { Authorization: `Bearer ${token}` },
  });
  const body = await response.json();
  expect(body.data.length).toBeLessThanOrEqual(100); // Max page size
});

SAST, DAST, and IAST

SAST (Static Application Security Testing)

SAST analyzes source code without executing the application. It catches vulnerabilities early in the development lifecycle.

Semgrep -- Fast, open-source static analysis with custom rules:

# Run Semgrep with OWASP rules
semgrep --config=p/owasp-top-ten src/

# Custom rule to detect hardcoded secrets
# .semgrep/hardcoded-secrets.yml
rules:
  - id: hardcoded-api-key
    patterns:
      - pattern: |
          const $KEY = "..."
      - metavariable-regex:
          metavariable: $KEY
          regex: (api_key|apiKey|API_KEY|secret|SECRET|password|PASSWORD)
    message: "Hardcoded secret detected in $KEY"
    severity: ERROR
    languages: [typescript, javascript]

CodeQL -- GitHub's semantic code analysis engine:

# .github/workflows/codeql.yml
name: CodeQL Analysis

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'

jobs:
  analyze:
    runs-on: ubuntu-latest
    permissions:
      security-events: write

    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: javascript-typescript
          queries: security-extended
      - uses: github/codeql-action/analyze@v3

DAST (Dynamic Application Security Testing)

DAST tests running applications by sending malicious inputs and analyzing responses.

OWASP ZAP -- The most widely used open-source DAST tool:

# ZAP baseline scan in CI
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
  -t https://staging.example.com \
  -r report.html \
  -c zap-config.conf

# ZAP full scan (more thorough, takes longer)
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-full-scan.py \
  -t https://staging.example.com \
  -r full-report.html

ZAP API scan for OpenAPI specs:

docker run -t ghcr.io/zaproxy/zaproxy:stable zap-api-scan.py \
  -t https://staging.example.com/openapi.json \
  -f openapi \
  -r api-report.html

IAST (Interactive Application Security Testing)

IAST instruments the application runtime to detect vulnerabilities during normal testing. It observes how data flows through the application and detects when tainted input reaches a sensitive sink (database query, file operation, response rendering).

IAST tools include Contrast Security, Checkmarx IAST, and Synopsys Seeker. They integrate as agents within the application runtime and report vulnerabilities as tests execute.


XSS (Cross-Site Scripting) Testing

XSS remains one of the most common web vulnerabilities. Test all user-input rendering paths:

test.describe('XSS Prevention', () => {
  const xssPayloads = [
    '<script>alert("xss")</script>',
    '<img src=x onerror=alert(1)>',
    '<svg onload=alert(1)>',
    'javascript:alert(1)',
    '"><script>alert(1)</script>',
    "'-alert(1)-'",
    '<body onload=alert(1)>',
    '<input onfocus=alert(1) autofocus>',
    '<details open ontoggle=alert(1)>',
    '<a href="javascript:alert(1)">click</a>',
  ];

  for (const payload of xssPayloads) {
    test(`sanitizes XSS: ${payload.substring(0, 30)}`, async ({ request }) => {
      // Store payload via API
      const createRes = await request.post('/api/comments', {
        data: { text: payload },
        headers: { Authorization: `Bearer ${token}` },
      });

      if (createRes.status() === 201) {
        const comment = await createRes.json();
        // Retrieve and verify sanitization
        const getRes = await request.get(`/api/comments/${comment.id}`);
        const body = await getRes.json();

        expect(body.text).not.toContain('<script');
        expect(body.text).not.toContain('onerror=');
        expect(body.text).not.toContain('onload=');
        expect(body.text).not.toContain('javascript:');
      } else {
        // Input validation rejected the payload -- also acceptable
        expect(createRes.status()).toBe(400);
      }
    });
  }
});

Fixing XSS -- the correct approach:

// VULNERABLE: rendering user input as raw HTML
app.get('/comments/:id', (req, res) => {
  const comment = getComment(req.params.id);
  res.send(`<div>${comment.text}</div>`); // XSS vulnerability
});

// SECURE: use a template engine with auto-escaping
// or sanitize with DOMPurify on the server
import DOMPurify from 'isomorphic-dompurify';

app.get('/comments/:id', (req, res) => {
  const comment = getComment(req.params.id);
  const sanitized = DOMPurify.sanitize(comment.text);
  res.send(`<div>${sanitized}</div>`);
});

CSRF (Cross-Site Request Forgery) Testing

CSRF attacks trick authenticated users into making unintended requests:

test.describe('CSRF Protection', () => {
  test('state-changing requests require CSRF token', async ({ request }) => {
    // Attempt POST without CSRF token
    const response = await request.post('/api/account/change-email', {
      data: { email: 'attacker@evil.com' },
      headers: {
        Authorization: `Bearer ${token}`,
        // Deliberately omitting CSRF token
      },
    });
    expect(response.status()).toBe(403);
  });

  test('CSRF token from one session cannot be used in another', async ({ request }) => {
    // Get CSRF token from session A
    const sessionARes = await request.get('/api/csrf-token');
    const { csrfToken: tokenA } = await sessionARes.json();

    // Try to use session A's CSRF token in session B
    const response = await request.post('/api/account/change-email', {
      data: { email: 'test@test.com' },
      headers: {
        Authorization: `Bearer ${sessionBToken}`,
        'X-CSRF-Token': tokenA,
      },
    });
    expect(response.status()).toBe(403);
  });
});

DevSecOps: Automated Security in CI/CD

A complete DevSecOps pipeline integrates security testing at every stage:

# .github/workflows/devsecops.yml
name: DevSecOps Pipeline

on:
  pull_request:
    branches: [main]

jobs:
  # Stage 1: Secret scanning (pre-commit)
  secret-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2

  # Stage 2: SAST (code analysis)
  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Semgrep SAST
        uses: semgrep/semgrep-action@v1
        with:
          config: >-
            p/owasp-top-ten
            p/typescript
            p/nodejs

  # Stage 3: Dependency scanning
  dependency-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Snyk Dependency Scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high

  # Stage 4: Container scanning
  container-scan:
    runs-on: ubuntu-latest
    needs: [sast]
    steps:
      - uses: actions/checkout@v4
      - name: Build Docker image
        run: docker build -t app:test .
      - name: Trivy Container Scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'app:test'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

  # Stage 5: DAST (dynamic testing)
  dast:
    runs-on: ubuntu-latest
    needs: [sast, dependency-scan]
    steps:
      - uses: actions/checkout@v4
      - name: Start application
        run: docker compose up -d
      - name: Wait for app
        run: |
          for i in {1..30}; do
            curl -s http://localhost:3000/health && break
            sleep 2
          done
      - name: ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: 'http://localhost:3000'
          rules_file_name: '.zap/rules.tsv'

  # Stage 6: Security test suite
  security-tests:
    runs-on: ubuntu-latest
    needs: [sast]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx playwright install --with-deps
      - name: Run Security Tests
        run: npx playwright test tests/security/ --project=chromium

Testing AI-Generated Code for Security

AI-generated code introduces unique security risks. LLMs may generate code that is syntactically correct and functionally working but contains subtle security vulnerabilities:

  • Using eval() or Function() for dynamic code execution
  • Missing input validation on user-supplied data
  • Hardcoded credentials or API keys in generated examples
  • Insecure random number generation (Math.random instead of crypto)
  • Missing HTTPS enforcement
  • Overly permissive CORS configurations

Automated checks for AI-generated code:

# .semgrep/ai-code-security.yml
rules:
  - id: no-eval
    pattern: eval(...)
    message: "eval() is dangerous -- use JSON.parse() or a safe alternative"
    severity: ERROR
    languages: [javascript, typescript]

  - id: no-math-random-security
    pattern: Math.random()
    message: "Use crypto.randomUUID() or crypto.getRandomValues() for security-sensitive randomness"
    severity: WARNING
    languages: [javascript, typescript]

  - id: no-hardcoded-credentials
    patterns:
      - pattern: |
          $VAR = "..."
      - metavariable-regex:
          metavariable: $VAR
          regex: (password|secret|apiKey|api_key|token|credential)
    message: "Possible hardcoded credential"
    severity: ERROR
    languages: [javascript, typescript]

Security Testing Checklist

Use this checklist for every release:

Authentication:

  • Password hashing uses bcrypt/argon2 with appropriate cost factor
  • JWT tokens have reasonable expiration times
  • Refresh token rotation is implemented
  • Account lockout after failed attempts
  • Multi-factor authentication for sensitive operations

Authorization:

  • Every endpoint checks user permissions
  • IDOR protection on all resource endpoints
  • Admin functions are properly gated
  • API keys have appropriate scopes

Input Validation:

  • All inputs are validated server-side (client-side validation is insufficient)
  • SQL injection prevention via parameterized queries
  • XSS prevention via output encoding and CSP headers
  • File upload validation (type, size, content)
  • Rate limiting on all public endpoints

Data Protection:

  • TLS 1.2+ for all connections
  • Sensitive data encrypted at rest
  • PII is not logged
  • API responses do not leak internal implementation details

Dependencies:

  • No known critical or high vulnerabilities in dependencies
  • Container base images are scanned and up to date
  • Third-party integrations use least-privilege API keys

Common Vulnerability Patterns and Fixes

Mass Assignment

// VULNERABLE: accepting all fields from request body
app.post('/api/users', async (req, res) => {
  const user = await User.create(req.body); // Attacker can set isAdmin: true
  res.json(user);
});

// SECURE: whitelist allowed fields
app.post('/api/users', async (req, res) => {
  const { name, email, password } = req.body;
  const user = await User.create({ name, email, password });
  res.json(user);
});

Path Traversal

// VULNERABLE: user-controlled file path
app.get('/files/:name', (req, res) => {
  const filePath = path.join('/uploads', req.params.name);
  res.sendFile(filePath); // Attacker sends ../../etc/passwd
});

// SECURE: validate and sanitize path
app.get('/files/:name', (req, res) => {
  const safeName = path.basename(req.params.name); // Strips directory traversal
  const filePath = path.join('/uploads', safeName);
  if (!filePath.startsWith('/uploads/')) {
    return res.status(400).json({ error: 'Invalid file path' });
  }
  res.sendFile(filePath);
});

Insecure Deserialization

// VULNERABLE: deserializing untrusted data
app.post('/api/import', (req, res) => {
  const data = JSON.parse(req.body.serialized);
  // If using a library that supports code execution during deserialization,
  // this is a remote code execution vulnerability
});

// SECURE: validate schema after deserialization
import { z } from 'zod';

const ImportSchema = z.object({
  items: z.array(z.object({
    name: z.string().max(200),
    value: z.number().positive(),
  })).max(1000),
});

app.post('/api/import', (req, res) => {
  const result = ImportSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json({ errors: result.error.issues });
  }
  // Process validated data
});

Conclusion

Security testing in 2026 is a continuous, automated practice woven into every stage of the software development lifecycle. Start with the OWASP Top 10 as your threat model, automate SAST and dependency scanning in CI, run DAST against staging environments, and build a comprehensive security test suite that covers authentication, authorization, injection, and data protection.

The tools are mature, free, and well-integrated with modern CI/CD platforms. The cost of not testing is measured in data breaches, regulatory fines, and lost customer trust. Make security testing a non-negotiable part of your quality assurance workflow.

For teams using AI coding agents, install security testing skills to ensure your agent follows secure coding practices:

npx @qaskills/cli add security-testing-owasp

Browse all 450+ QA skills at qaskills.sh/skills.

Security Testing Complete Guide: OWASP, Tools, and Automation in 2026 | QASkills.sh