|

Shift-Left Security Testing: SAST, DAST, and OWASP Top 10 for QA Engineers

Contents

Shift-Left Security Testing: SAST, DAST, and OWASP Top 10 for QA Engineers

The security landscape of software development has fundamentally changed in 2026. With AI-generated code becoming mainstream, release cycles accelerating to multiple deployments per day, and attack surfaces expanding through APIs, microservices, and third-party integrations, the traditional model of security testing as a final gate before release is dangerously insufficient. QA engineers are uniquely positioned to embed security testing into the development workflow, catching vulnerabilities early when they are cheapest and easiest to fix. This is the essence of shift-left security testing.

🎭 Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.

This guide is written specifically for QA engineers who want to add security testing to their skill set. You do not need to be a penetration tester or security specialist to make a significant impact. By understanding the OWASP Top 10 vulnerabilities, learning the difference between SAST and DAST tools, and integrating security gates into your CI/CD pipeline, you can prevent the most common security issues from reaching production. Every concept includes practical, actionable code examples that you can implement today.

Why QA Engineers Need Security Testing Skills in 2026

Three converging trends make security testing skills essential for QA engineers in 2026. First, AI-generated code is now responsible for a significant percentage of production code in many organizations. While AI assistants produce functional code quickly, they often introduce security vulnerabilities because they optimize for functionality rather than security. Someone needs to catch these vulnerabilities before they reach production, and QA engineers are already positioned in the quality assurance workflow to do so.

Second, release velocity continues to accelerate. Organizations deploying multiple times per day cannot afford to wait for periodic security audits. Security must be embedded in the same CI/CD pipeline that runs functional tests. QA engineers who already manage test automation in CI/CD are the natural owners of security automation in that pipeline.

Third, the attack surface has expanded dramatically. Modern applications communicate through dozens of APIs, consume third-party services, process user-generated content, and run in distributed cloud environments. Each integration point is a potential vulnerability. QA engineers who test these integration points for functional correctness can extend their testing to cover security concerns with relatively modest additional effort.

OWASP Top 10 Explained for QA Engineers

The OWASP Top 10 is the industry standard list of the most critical web application security risks. Understanding these risks is the foundation of effective security testing. Here is each category explained from a QA engineer’s perspective, with emphasis on what you can test and how.

A01: Broken Access Control

Broken access control occurs when users can access resources or perform actions beyond their intended permissions. This includes horizontal privilege escalation (accessing another user’s data) and vertical privilege escalation (performing admin actions as a regular user). As a QA engineer, you can test for this by attempting to access resources with different user roles and verifying that unauthorized access is properly denied.

// Playwright test for broken access control
import { test, expect } from '@playwright/test';

test.describe('Access Control Tests', () => {
  test('regular user cannot access admin API endpoints', async ({ request }) => {
    // Authenticate as regular user
    const loginResp = await request.post('/api/auth/login', {
      data: { email: 'user@example.com', password: 'userpass' }
    });
    const { token } = await loginResp.json();

    // Attempt admin-only endpoints
    const adminEndpoints = [
      '/api/admin/users',
      '/api/admin/settings',
      '/api/admin/audit-logs',
      '/api/admin/roles'
    ];

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

  test('user cannot access another user data via IDOR', async ({ request }) => {
    // Login as user A
    const loginResp = await request.post('/api/auth/login', {
      data: { email: 'userA@example.com', password: 'passA' }
    });
    const { token } = await loginResp.json();

    // Try to access user B's profile by manipulating the ID
    const response = await request.get('/api/users/user-b-id/profile', {
      headers: { Authorization: `Bearer ${token}` }
    });
    expect(response.status()).toBe(403); // Should not return 200
  });
});

A02: Cryptographic Failures

Cryptographic failures involve inadequate protection of sensitive data in transit and at rest. As a QA engineer, verify that HTTPS is enforced, sensitive data is not exposed in URLs or logs, and API responses do not leak unnecessary information. Check that password fields are masked, session tokens are transmitted securely, and sensitive data like credit card numbers or social security numbers are never fully exposed in API responses.

A03: Injection

Injection attacks occur when untrusted data is sent to an interpreter as part of a command or query. SQL injection, NoSQL injection, and command injection are the most common forms. QA engineers can test for injection by sending malicious payloads through input fields and API parameters, then verifying that the application handles them safely without executing the injected code.

// Testing for SQL injection via API
test('API is protected against SQL injection', async ({ request }) => {
  const sqlInjectionPayloads = [
    "' OR '1'='1",
    "'; DROP TABLE users; --",
    "' UNION SELECT * FROM users --",
    "1; UPDATE users SET role='admin' WHERE id=1",
    "' OR 1=1 --",
  ];

  for (const payload of sqlInjectionPayloads) {
    const response = await request.get('/api/products/search', {
      params: { q: payload }
    });
    // Should return 400 Bad Request or empty results, NOT a 500
    expect(response.status(), `Payload "${payload}" should be handled safely`)
      .not.toBe(500);
    const body = await response.json();
    // Should not return all records (sign of successful injection)
    if (response.status() === 200) {
      expect(body.results.length).toBeLessThan(100);
    }
  }
});

A04: Insecure Design

Insecure design refers to architectural and design flaws that cannot be fixed by implementation alone. As a QA engineer, you can identify insecure design during requirements review by asking questions like: What happens if a user attempts this action ten thousand times? What data is exposed if this API endpoint is called without authentication? Are there rate limits on sensitive operations like password reset or payment processing?

A05: Security Misconfiguration

Security misconfiguration includes default credentials, unnecessary open ports, verbose error messages exposing stack traces, missing security headers, and overly permissive CORS policies. These are among the easiest vulnerabilities for QA engineers to test because they often require nothing more than inspecting HTTP headers and error responses.

// Testing security headers
test('verify security headers are present', async ({ request }) => {
  const response = await request.get('/');
  const headers = response.headers();

  // Essential security headers
  expect(headers['x-content-type-options']).toBe('nosniff');
  expect(headers['x-frame-options']).toMatch(/DENY|SAMEORIGIN/);
  expect(headers['strict-transport-security']).toBeDefined();
  expect(headers['content-security-policy']).toBeDefined();
  expect(headers['x-xss-protection']).toBeDefined();

  // Should NOT expose server information
  expect(headers['server']).not.toMatch(/Apache|nginx|Express/i);
  expect(headers['x-powered-by']).toBeUndefined();
});

// Testing verbose error messages
test('error responses do not expose stack traces', async ({ request }) => {
  const response = await request.get('/api/nonexistent-endpoint');
  const body = await response.text();

  // Should not contain stack traces or internal paths
  expect(body).not.toMatch(/at\s+\w+.*\.\w+:\d+:\d+/); // Stack trace pattern
  expect(body).not.toMatch(/\/home\/|\/var\/|\/usr\//); // Server paths
  expect(body).not.toContain('node_modules');
  expect(body).not.toContain('.java:');
});

A06: Vulnerable and Outdated Components

Using components with known vulnerabilities is a pervasive risk. While QA engineers may not manage dependency updates directly, they can integrate vulnerability scanning into CI/CD pipelines using tools like npm audit, Snyk, or Dependabot. Adding a vulnerability scan step to your test pipeline ensures that known vulnerabilities are caught before deployment.

🚀 Level Up Your Playwright

From locators to CI pipelines — build a production-grade Playwright + TypeScript framework step by step.

A07: Identification and Authentication Failures

Authentication failures include weak password policies, missing brute force protection, session fixation, and improper session management. QA engineers should test password complexity requirements, account lockout after failed attempts, session expiration, and proper logout behavior. These are functional tests with security implications.

// Testing authentication security
test('account locks after 5 failed login attempts', async ({ request }) => {
  const email = 'locktest@example.com';

  // Attempt 5 failed logins
  for (let i = 0; i < 5; i++) {
    await request.post('/api/auth/login', {
      data: { email, password: 'wrongpassword' }
    });
  }

  // 6th attempt should be locked even with correct password
  const response = await request.post('/api/auth/login', {
    data: { email, password: 'correctpassword' }
  });
  expect(response.status()).toBe(429); // Too Many Requests
  const body = await response.json();
  expect(body.message).toContain('locked');
});

test('session token is invalidated after logout', async ({ request }) => {
  // Login
  const loginResp = await request.post('/api/auth/login', {
    data: { email: 'user@example.com', password: 'password' }
  });
  const { token } = await loginResp.json();

  // Logout
  await request.post('/api/auth/logout', {
    headers: { Authorization: `Bearer ${token}` }
  });

  // Try to use the old token
  const response = await request.get('/api/profile', {
    headers: { Authorization: `Bearer ${token}` }
  });
  expect(response.status()).toBe(401); // Token should be invalid
});

A08: Software and Data Integrity Failures

This category covers insecure CI/CD pipelines, unsigned updates, and deserialization vulnerabilities. QA engineers can verify that software updates use integrity checks, that CI/CD configurations are version-controlled, and that the application properly validates data integrity for critical operations.

A09: Security Logging and Monitoring Failures

Insufficient logging makes it impossible to detect and respond to security breaches. QA engineers can verify that security-relevant events are logged: failed login attempts, access control failures, input validation failures, and changes to sensitive data. Test that these logs include sufficient context like timestamps, user IDs, and request details without including sensitive data like passwords or tokens.

A10: Server-Side Request Forgery (SSRF)

SSRF occurs when an application fetches a remote resource based on user-supplied input without proper validation. If your application has features that accept URLs as input such as webhook configurations, image URL imports, or link previews, test that it rejects internal network addresses, localhost references, and cloud metadata endpoints.

// Testing for SSRF
test('API rejects SSRF attempts in URL input', async ({ request }) => {
  const ssrfPayloads = [
    'http://localhost/admin',
    'http://127.0.0.1/admin',
    'http://169.254.169.254/latest/meta-data/', // AWS metadata
    'http://[::1]/admin',
    'http://0.0.0.0/admin',
    'http://internal-service.local/secrets',
  ];

  for (const payload of ssrfPayloads) {
    const response = await request.post('/api/webhooks', {
      data: { url: payload, event: 'test' }
    });
    expect(response.status(), `SSRF payload ${payload} should be rejected`)
      .toBeGreaterThanOrEqual(400);
  }
});

SAST vs. DAST: What Each Catches

Static Application Security Testing (SAST) analyzes source code without executing it, identifying vulnerabilities like hardcoded credentials, unsafe function calls, injection-prone code patterns, and insecure cryptographic usage. Dynamic Application Security Testing (DAST) tests the running application from the outside, simulating attacks to find vulnerabilities like XSS, CSRF, authentication bypasses, and security misconfigurations.

SAST tools for QA engineers include Semgrep, which is open source and highly customizable with simple YAML rule definitions, and CodeQL from GitHub, which provides deep semantic code analysis. For DAST, OWASP ZAP is the industry standard open-source tool that can be integrated into CI/CD pipelines for automated security scanning. Both approaches are complementary: SAST catches vulnerabilities in code before deployment, while DAST catches vulnerabilities in the running application’s configuration and behavior.

Adding Security Gates in CI/CD with GitHub Actions

# .github/workflows/security-gates.yml
name: Security Gates
on:
  pull_request:
    branches: [main]

jobs:
  sast-scan:
    name: SAST - Static Analysis
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Semgrep SAST
        uses: returntocorp/semgrep-action@v1
        with:
          config: >-
            p/owasp-top-ten
            p/javascript
            p/typescript
        env:
          SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}

  dependency-scan:
    name: Dependency Vulnerability Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - name: Run npm audit
        run: npm audit --audit-level=high
        continue-on-error: false

  dast-scan:
    name: DAST - Dynamic Analysis
    needs: [sast-scan]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Start application
        run: |
          npm ci
          npm run build
          npm start &
          sleep 10

      - name: OWASP ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: 'http://localhost:3000'
          rules_file_name: '.zap/rules.tsv'
          cmd_options: '-a'

  security-tests:
    name: Playwright Security Tests
    needs: [sast-scan]
    runs-on: ubuntu-latest
    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 Test Suite
        run: npx playwright test --project=security
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: security-test-results
          path: playwright-report/

Playwright-Based Security Tests

Playwright is not just a functional testing tool — it is an excellent platform for automated security testing. Its ability to intercept network requests, manipulate cookies, inject JavaScript, and control browser behavior makes it ideal for testing XSS prevention, authentication flows, CSRF protection, and rate limiting. Here are practical examples for several OWASP categories.

// xss-prevention.spec.ts - Cross-Site Scripting tests
import { test, expect } from '@playwright/test';

test.describe('XSS Prevention Tests', () => {
  const xssPayloads = [
    '<script>alert("XSS")</script>',
    '<img src=x onerror=alert("XSS")>',
    '<svg/onload=alert("XSS")>',
    'javascript:alert("XSS")',
    '<iframe src="javascript:alert(1)">',
    '" onfocus="alert(1)" autofocus="',
  ];

  for (const payload of xssPayloads) {
    test(`input field sanitizes: ${payload.slice(0, 30)}...`, async ({ page }) => {
      await page.goto('/search');
      await page.fill('[data-testid="search-input"]', payload);
      await page.click('[data-testid="search-button"]');

      // Check that the payload is displayed as text, not executed
      const resultText = await page.locator('[data-testid="search-results"]').innerHTML();
      expect(resultText).not.toContain('<script');
      expect(resultText).not.toContain('onerror=');
      expect(resultText).not.toContain('onload=');

      // Verify no JavaScript alerts were triggered
      let alertTriggered = false;
      page.on('dialog', () => { alertTriggered = true; });
      await page.waitForTimeout(1000);
      expect(alertTriggered).toBe(false);
    });
  }
});

// rate-limiting.spec.ts
test('API enforces rate limiting', async ({ request }) => {
  const endpoint = '/api/auth/login';
  const responses: number[] = [];

  // Send 100 requests rapidly
  for (let i = 0; i < 100; i++) {
    const resp = await request.post(endpoint, {
      data: { email: 'test@example.com', password: 'test' }
    });
    responses.push(resp.status());
  }

  // At least some should be rate-limited (429)
  const rateLimited = responses.filter(s => s === 429);
  expect(rateLimited.length).toBeGreaterThan(0);
});

These security tests can run alongside your functional test suite in CI/CD, providing continuous security validation with every code change. They do not replace professional penetration testing, but they catch the most common vulnerabilities automatically and continuously, which is far more effective than periodic manual security assessments.

Building Your Security Testing Skills Roadmap

Start with the fundamentals: understand the OWASP Top 10 and implement basic security header checks and authentication tests. Then expand to input validation testing across all user-facing endpoints. Add SAST scanning with Semgrep to your CI/CD pipeline. Integrate OWASP ZAP for automated DAST scanning. Finally, develop custom Playwright security tests for your application’s specific risk areas. Each step builds on the previous one, and the entire progression can be completed within a few months of focused effort.

The QA engineer who combines functional testing expertise with security testing skills is exceptionally valuable in 2026. You bring the testing mindset, the automation infrastructure, and the CI/CD integration experience that security testing requires. Adding the security knowledge layer transforms you from a quality assurance professional into a quality and security assurance professional, a role that every modern development team needs.

Frequently Asked Questions

Q: Do I need to be a security expert to implement security testing as a QA engineer?

A: No. You do not need deep expertise in cryptography or exploit development. Start with the OWASP Top 10, which covers the most common vulnerabilities. Implement automated checks for security headers, input validation, access control, and authentication flows. These tests catch eighty percent of common vulnerabilities and require only a solid understanding of HTTP, APIs, and your existing testing skills. You can progressively deepen your security knowledge over time.

Q: How do I integrate OWASP ZAP into a CI/CD pipeline without slowing down deployments?

A: Use ZAP’s baseline scan mode for pull request checks, which runs quickly and catches the most obvious issues. Reserve full active scans for nightly or weekly scheduled runs. Configure ZAP rules to suppress known false positives and focus on high-severity findings. The baseline scan typically completes in five to ten minutes and integrates cleanly with GitHub Actions, GitLab CI, and Jenkins through official Docker images and actions.

Q: Should security tests be separate from functional tests or combined in the same suite?

A: Use a hybrid approach. Embed basic security assertions within functional tests where they naturally fit, such as verifying access control during role-based testing or checking for XSS when testing input forms. Create a separate dedicated security test project for comprehensive security-specific scenarios like injection testing, rate limit verification, and security header validation. This dedicated project can run on a different schedule or as a separate CI job. The combined approach ensures broad security coverage without slowing down the functional test feedback loop.

🎓 Master Playwright End to End

Join hundreds of SDETs building real automation frameworks. Lifetime access, hands-on projects, and a job-ready portfolio.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.