Back to Blog

OWASP Top 10 Explained for QA Engineers: How to Test for Critical Vulnerabilities

Security testing isn't just for penetration testers. Learn how QA engineers can identify and test for the OWASP Top 10vulnerabilities, from SQL injection to broken access control, and integrate security into your testing workflow.

Scanly App

Published

11 min read

Reading time

Related articles: Also see a hands-on guide to testing for the full range of web vulnerabilities, deep-dive testing and prevention strategies for XSS attacks, and testing for IDOR vulnerabilities and the access control flaw behind data breaches.

OWASP Top 10 Explained for QA Engineers: How to Test for Critical Vulnerabilities

Security vulnerabilities cost companies millions in breaches, lost trust, and regulatory fines. Yet many QA teams focus exclusively on functional testing, leaving security as an afterthought�or worse, someone else's problem.

The reality is that security is everyone's responsibility, and QA engineers are uniquely positioned to catch vulnerabilities before they reach production. The OWASP Top 10 provides a starting point: a list of the most critical web application security risks, updated regularly based on real-world data.

This guide explains each vulnerability in the OWASP Top 10 and, more importantly, shows you how to test for them as part of your QA workflow. You don't need to be a penetration tester�just a conscientious QA engineer who understands what to look for.

What is the OWASP Top 10?

The Open Web Application Security Project (OWASP) Top 10 is a standard awareness document representing a broad consensus about the most critical security risks to web applications. It's updated every 3-4 years based on data from security firms, bug bounty programs, and incident reports.

The 2021 edition (still relevant in 2026) includes:

  1. Broken Access Control
  2. Cryptographic Failures
  3. Injection
  4. Insecure Design
  5. Security Misconfiguration
  6. Vulnerable and Outdated Components
  7. Identification and Authentication Failures
  8. Software and Data Integrity Failures
  9. Security Logging and Monitoring Failures
  10. Server-Side Request Forgery (SSRF)

Let's dive into each, with practical testing guidance.

1. Broken Access Control

What It Is

Users can access data or functions they shouldn't. For example:

  • Changing a URL parameter to view someone else's account
  • Accessing an admin panel without proper permissions
  • Modifying HTTP requests to bypass authorization checks

Example Vulnerability

# User sees their own profile
GET /api/users/12345/profile

# User changes ID to view another user's profile (should be blocked!)
GET /api/users/67890/profile

If the server doesn't validate that user 12345 is authorized to view user 67890's data, you have broken access control.

How to Test

Test Type How to Perform
URL manipulation Change IDs, usernames, or slugs in URLs; attempt to access other users' resources
Role escalation Log in as a regular user, attempt to access admin endpoints
HTTP verb tampering If DELETE is blocked, try POST with _method=DELETE
Cookie/token manipulation Modify JWTs, session cookies, or authorization headers
Forced browsing Directly navigate to /admin, /debug, or other restricted paths

Playwright Test Example

test('should not allow user to access another user's data', async ({ request }) => {
  // Login as user1
  const user1Token = await loginAs('user1@example.com');

  // Try to access user2's profile
  const response = await request.get('/api/users/user2-id/profile', {
    headers: { 'Authorization': `Bearer ${user1Token}` }
  });

  // Should be blocked
  expect(response.status()).toBe(403); // Forbidden
});

2. Cryptographic Failures

What It Is

Sensitive data (passwords, credit cards, PII) exposed due to:

  • Transmitting data over HTTP instead of HTTPS
  • Weak encryption algorithms (MD5, SHA1)
  • Hardcoded encryption keys
  • Storing passwords in plaintext or with weak hashing

How to Test

Test Type How to Perform
Protocol check Use browser DevTools to verify all requests use HTTPS
Password storage Check database or logs�passwords should be hashed (bcrypt, Argon2), never plaintext
Sensitive data in URLs Ensure tokens, passwords aren't in query params (logged by proxies, browsers)
SSL/TLS validation Use SSL Labs or nmap to check for weak ciphers
Local storage inspection Check if sensitive data is stored in localStorage/sessionStorage

Automated Check

test('should use HTTPS for all API calls', async ({ page }) => {
  page.on('request', (request) => {
    const url = request.url();
    if (url.startsWith('http://') && !url.includes('localhost')) {
      throw new Error(`Insecure HTTP request detected: ${url}`);
    }
  });

  await page.goto('https://myapp.com');
  await page.click('button[data-testid="submit"]');
  // If any HTTP request is made, test fails
});

3. Injection

What It Is

Untrusted data is sent to an interpreter (SQL, OS command, LDAP) without validation, allowing attackers to execute malicious commands.

SQL Injection Example:

-- User input: ' OR '1'='1
-- Resulting query:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'anything';
-- Returns all users!

How to Test

| Test Type | How to Perform | | --------------------- | ------------------------------------------------------------------------ | -------- | | SQL injection | Input: ' OR '1'='1, '; DROP TABLE users;--, 1' UNION SELECT NULL-- | | Command injection | Input: ; ls -la, & whoami, | cat /etc/passwd | | NoSQL injection | Input: {"$ne": null} in JSON payloads | | LDAP injection | Input: _)(uid=_))( | (uid=\* | | XPath injection | Input: ' or '1'='1 |

Test Example with Playwright

test('should prevent SQL injection in search field', async ({ page }) => {
  await page.goto('https://myapp.com/search');

  const maliciousInputs = ["' OR '1'='1", "'; DROP TABLE users;--", "1' UNION SELECT NULL, NULL, NULL--"];

  for (const input of maliciousInputs) {
    await page.fill('input[name="search"]', input);
    await page.click('button[type="submit"]');

    // Should show error or no results, not crash or return all data
    const errorMessage = await page.locator('.error-message');
    expect(await errorMessage.count()).toBeGreaterThan(0);
  }
});

4. Insecure Design

What It Is

Flaws in the architecture or design, not implementation bugs. Examples:

  • No rate limiting on password reset (brute force attacks)
  • Allowing account enumeration (revealing which emails are registered)
  • Missing security requirements in user stories

How to Test

Test Type How to Perform
Rate limiting Make 100+ requests rapidly; should be throttled
Account enumeration Try registering existing email�error message shouldn't reveal if email exists
Threat modeling review Review designs with STRIDE or similar frameworks
Business logic abuse Try workflows in unexpected orders (e.g., checkout before adding items)

Rate Limiting Test

test('should rate limit password reset attempts', async ({ request }) => {
  const email = 'test@example.com';
  let blockedCount = 0;

  // Try 20 password resets
  for (let i = 0; i < 20; i++) {
    const response = await request.post('/api/auth/reset-password', {
      data: { email },
    });

    if (response.status() === 429) {
      // Too Many Requests
      blockedCount++;
    }
  }

  // After a certain number, should be rate limited
  expect(blockedCount).toBeGreaterThan(0);
});

5. Security Misconfiguration

What It Is

Insecure default settings, incomplete configs, open cloud storage, verbose error messages revealing system details.

Examples:

  • Default admin/admin credentials
  • Directory listing enabled
  • Detailed stack traces shown to users
  • Unnecessary services enabled

How to Test

Test Type How to Perform
Default credentials Try admin/admin, root/root, etc.
Error message detail Trigger errors; check if stack traces or DB details are exposed
HTTP headers Check for security headers (CSP, HSTS, X-Frame-Options)
Unnecessary features Look for debug endpoints, test pages in production
Directory listing Navigate to /uploads/, /assets/�shouldn't show file lists

Security Headers Test

test('should have security headers', async ({ page }) => {
  const response = await page.goto('https://myapp.com');
  const headers = response.headers();

  // Check for critical security headers
  expect(headers['strict-transport-security']).toBeDefined();
  expect(headers['x-content-type-options']).toBe('nosniff');
  expect(headers['x-frame-options']).toMatch(/DENY|SAMEORIGIN/);
  expect(headers['content-security-policy']).toBeDefined();
});

6. Vulnerable and Outdated Components

What It Is

Using libraries with known vulnerabilities (e.g., old versions of React, jQuery, OpenSSL).

The 2017 Equifax breach was caused by an unpatched Apache Struts vulnerability�a perfect example of this risk.

How to Test

Test Type How to Perform
Dependency scanning Run npm audit, yarn audit, or use Snyk, Dependabot
Version detection Check <meta> tags, JS bundles, HTTP headers for version info
Known CVEs Search CVE databases for identified library versions

Automated Dependency Check (CI/CD)

# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm audit --audit-level=high # Fail on high/critical vulnerabilities

7. Identification and Authentication Failures

What It Is

Weak authentication allowing account takeover:

  • Weak password requirements
  • No multi-factor authentication (MFA)
  • Session tokens don't expire
  • Credential stuffing (reusing leaked passwords)

How to Test

Test Type How to Perform
Weak passwords Try registering with "password", "123456"
Session management Check if sessions expire after logout or timeout
MFA bypass Try accessing protected resources without completing MFA
Password reset flaws Check if reset tokens are guessable or reusable

Session Expiry Test

test('session should expire after logout', async ({ page, context }) => {
  // Login
  await page.goto('https://myapp.com/login');
  await page.fill('input[name="email"]', 'user@example.com');
  await page.fill('input[name="password"]', 'SecurePass123!');
  await page.click('button[type="submit"]');

  // Store cookies
  const cookies = await context.cookies();

  // Logout
  await page.click('button[data-testid="logout"]');

  // Try to access protected resource with old session
  await context.addCookies(cookies);
  await page.goto('https://myapp.com/dashboard');

  // Should redirect to login
  expect(page.url()).toContain('/login');
});

8. Software and Data Integrity Failures

What It Is

  • Using libraries from untrusted CDNs without integrity checks
  • Insecure CI/CD pipelines allowing code injection
  • Unsigned software updates

How to Test

Test Type How to Perform
Subresource Integrity (SRI) Check that <script> tags from CDNs have integrity attributes
Build reproducibility Verify builds are deterministic and traceable
Supply chain verification Use tools like Sigstore, verify npm package signatures

Check for SRI

test('CDN scripts should use Subresource Integrity', async ({ page }) => {
  await page.goto('https://myapp.com');

  const externalScripts = await page.locator('script[src^="https://cdn"]').all();

  for (const script of externalScripts) {
    const integrity = await script.getAttribute('integrity');
    expect(integrity).toBeTruthy();
    expect(integrity).toMatch(/^sha(256|384|512)-/);
  }
});

9. Security Logging and Monitoring Failures

What It Is

Insufficient logging makes it impossible to detect breaches or investigate incidents.

Critical events that should be logged:

  • Login attempts (success and failure)
  • Access control failures
  • Input validation failures
  • Authentication token creation/use

How to Test

Test Type How to Perform
Log completeness Trigger security events (failed login, etc.); check logs
Log protection Ensure logs aren't publicly accessible
Alerting Verify alerts fire for suspicious activity (multiple failed logins)

10. Server-Side Request Forgery (SSRF)

What It Is

Attacker tricks server into making requests to internal resources or external systems.

Example:

# User provides URL
POST /api/fetch-image
{ "url": "http://internal-admin-panel/delete-all-users" }

# Server fetches the URL (bad!)

How to Test

Test Type How to Perform
Internal IP access Submit http://localhost, http://127.0.0.1, http://169.254.169.254 (AWS metadata)
Protocol smuggling Try file://, gopher://, dict:// protocols
Redirect following Check if server follows redirects to internal IPs

Integrating Security Testing into Your Workflow

1. Use SAST (Static Application Security Testing)

  • ESLint security plugins: Detect insecure patterns in code
  • SonarQube: Comprehensive code quality and security analysis
  • Semgrep: Lightweight, customizable static analysis

2. Use DAST (Dynamic Application Security Testing)

  • OWASP ZAP: Automated scanner for running applications
  • Burp Suite: Interception proxy for manual and automated testing

3. Include Security in Your Test Plan

## Test Plan: User Registration

### Functional Tests

- [ ] User can register with valid email
- [ ] Error shown for invalid email format

### Security Tests

- [ ] SQL injection blocked in email field
- [ ] Password must meet complexity requirements (OWASP Top 10 #7)
- [ ] HTTPS enforced for registration endpoint (OWASP Top 10 #2)
- [ ] Rate limiting on registration attempts (OWASP Top 10 #4)

Conclusion

Security testing doesn't have to be overwhelming. By understanding the OWASP Top 10 and integrating targeted tests into your QA workflow, you can catch critical vulnerabilities before they become breaches.

Start small: pick 2-3 vulnerabilities most relevant to your application and write tests for them. Expand from there. Security is a journey, not a destination�and every test you add makes your application more resilient.

Ready to build secure, reliable applications? Sign up for ScanlyApp and integrate security testing into your QA pipeline today.

Related Posts