Skip to content
← Blog

Proxying Playwright Through OWASP ZAP: A CI/CD Runbook for Continuous DAST

Master owasp zap playwright automated security testing. Learn how to turn regression suites into dynamic vulnerability scans with this runbook.

8 min readSimon Daniel März
Proxying Playwright Through OWASP ZAP: A CI/CD Runbook for Continuous DASTGenerated with the help of AI

Annual penetration tests find critical vulnerabilities months after the offending code shipped to production. For teams operating continuous delivery pipelines, a security assessment conducted once or twice a year is functionally obsolete before the PDF report hits an executive inbox. Meanwhile, standard Dynamic Application Security Testing (DAST) crawlers struggle with modern web applications: they fail on complex multi-step state machines, choke on client-side routing, and cannot navigate past modern single sign-on (SSO) or multi-factor authentication barriers.

Your quality assurance pipeline already contains the exact solution to this visibility problem. Your Playwright end-to-end (E2E) regression test suite already walks through complex business processes, authenticates across edge cases, fills forms with valid data, and reaches the deeply nested views that headless spiders miss.

By routing Playwright traffic through OWASP ZAP (Zed Attack Proxy) acting as an upstream intercepting proxy, you transform everyday functional testing into a continuous, automated security audit. Here is the operational runbook for implementing this architecture in your continuous integration (CI) pipeline.


The Spider Problem: Why Traditional DAST Fails Modern SPAs

Standalone DAST tools rely heavily on crawlers to discover attack surfaces. Traditional spiders follow basic HTML hyperlinks and submit discoverable <form> tags. In a legacy multi-page architecture, this approach mapped a reasonable percentage of an application's perimeter.

Modern single-page applications (built with React, Vue, Angular, or Svelte) break traditional crawlers completely:

  • Navigation occurs via JavaScript history manipulations rather than server-rendered hyperlinks.
  • State transitions depend on asynchronous REST or GraphQL payloads.
  • Interactive components require precise sequences of user interaction (e.g., clicking a toggle, opening a modal, confirming an action) before target endpoints are ever invoked.
  • Authentication tokens reside in session storage, memory, or HTTP-only cookies that spiders cannot reliably negotiate.

When a standalone scanner attempts to crawl a modern SaaS dashboard, it routinely covers less than 20% of the route tree before getting stuck in infinite loops or returning to the login screen.

By contrast, an engineering team's Playwright suite is purpose-built to simulate legitimate user behaviors. It understands how to log in, dismiss banners, interact with custom UI widgets, and trigger backend API calls. When Playwright drives the browser while OWASP ZAP listens in the middle, ZAP receives an exhaustive, pre-authenticated site map without needing an autonomous crawler to guess the way through.


Architectural Blueprint: The Intercepting Proxy Pattern

The integration operates across three distinct phases in the delivery pipeline:

  1. Proxy Daemon Initialization: OWASP ZAP launches in daemon mode inside a container, exposing an HTTP proxy port (typically 8080 or 8090) and an administrative API port.
  2. Proxied Test Execution (Passive Scanning): Playwright boots headless browser contexts configured to route all HTTP/HTTPS traffic through the ZAP proxy. As Playwright executes its functional regressions, ZAP records every request and response, passively inspecting headers, cookies, anti-CSRF tokens, and DOM contents for security issues without mutating payloads.
  3. Targeted Active Scanning (Attacking): Once the Playwright suite completes, the CI pipeline triggers an active scan against the exact URLs, parameters, and endpoints recorded in ZAP’s session history. ZAP injects targeted security test vectors (SQL injection, cross-site scripting, path traversal, header manipulation) into real application paths that have been verified to exist.
+-------------------------------------------------------------+
| CI/CD Pipeline Container Environment                        |
|                                                             |
|  +--------------------+        HTTP/S Proxy        +-----+  |
|  | Playwright Runner  | -------------------------> |     |  |
|  | (E2E Test Suite)   |                            |  Z  |  |
|  +--------------------+                            |  A  |  |
|            |                                       |  P  |  |
|            | Drives UI                             |     |  |
|            v                                       |     |  |
|  +--------------------+    Target Application      |  P  |  |
|  | Headless Chromium  | -------------------------> |  r  |  |
|  | Context            |   (via ZAP proxy host)     |  o  |  |
|  +--------------------+                            |  x  |  |
|                                                    |  y  |  |
|  +--------------------+        Scan Controls       |     |  |
|  | Pipeline Script /  | -------------------------> |     |  |
|  | ZAP REST Client    |   (Trigger Active Scan)    |     |  |
|  +--------------------+                            +-----+  |
+-------------------------------------------------------------+

This decoupled architecture allows teams to maintain their functional tests without polluting test logic with security assertions, while security teams gain continuous visibility into real user journeys.


Step-by-Step Implementation Runbook

Setting up owasp zap playwright automated security testing requires configuring container networking, managing TLS trust, configuring Playwright browser contexts, and orchestrating ZAP via its REST API.

Step 1: Deploy OWASP ZAP in CI

In an enterprise CI/CD runner (such as GitHub Actions, GitLab CI, or a local Docker Compose test harness), run the official OWASP ZAP Docker image in daemon mode. Ensure the API key is passed securely or explicitly disabled for private runner networks.

Below is an operational docker-compose.yml snippet illustrating an isolated test environment:

version: "3.8"

services:
  zap:
    image: ghcr.io/zaproxy/zaproxy:stable
    command: zap.sh -daemon -host 0.0.0.0 -port 8080 -config api.disablekey=true -config api.addrs.addr.name=.* -config api.addrs.addr.regex=true
    ports:
      - "8080:8080"
    volumes:
      - ./zap-reports:/zap/wrk:rw

  app-under-test:
    build:
      context: .
      dockerfile: Dockerfile.app
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=test
      - DATABASE_URL=postgres://test:test@db:5432/test_db

Step 2: Configure Playwright to Route Traffic via the Proxy

By default, modern browser engines reject self-signed certificates generated by intercepting proxies. When Playwright tunnels HTTPS traffic through ZAP, ZAP re-encrypts the connection with its internal Root Certificate Authority (CA).

You must explicitly instruct Playwright to route traffic through the proxy and handle certificate verification. You can either import the ZAP Root CA into the operating system certificate store or configure Playwright's ignoreHTTPSErrors setting within an isolated CI sandbox.

Here is an enterprise-grade Playwright configuration (playwright.config.ts) establishing proxy integration:

import { defineConfig, devices } from '@playwright/test';

const ZAP_PROXY_HOST = process.env.ZAP_PROXY_HOST || '127.0.0.1';
const ZAP_PROXY_PORT = process.env.ZAP_PROXY_PORT || '8080';
const USE_SECURITY_PROXY = process.env.ENABLE_SECURITY_PROXY === 'true';

export default defineConfig({
  testDir: './e2e',
  timeout: 60000,
  retries: 0, // Retries can distort passive scan logs
  use: {
    baseURL: process.env.APP_URL || 'http://localhost:3000',
    trace: 'retain-on-failure',
    // Route traffic through ZAP when the security flag is active
    ...(USE_SECURITY_PROXY && {
      proxy: {
        server: `http://${ZAP_PROXY_HOST}:${ZAP_PROXY_PORT}`,
      },
      // ZAP dynamically re-signs HTTPS traffic
      ignoreHTTPSErrors: true,
    }),
  },
  projects: [
    {
      name: 'chromium-security',
      use: { 
        ...devices['Desktop Chrome'],
        launchOptions: {
          args: [
            // Ensure WebRTC and non-proxied subchannels do not leak
            '--disable-features=IsolateOrigins,site-per-process',
          ],
        },
      },
    },
  ],
});

Step 3: Seed Application State via Playwright

Execute your standard Playwright functional tests against the staging instance. The test suite walks through normal user journeys:

import { test, expect } from '@playwright/test';

test.describe('Authenticated User Workflows', () => {
  test('User can update security profile parameters', async ({ page }) => {
    // Authenticate
    await page.goto('/login');
    await page.fill('input[name="username"]', '[email protected]');
    await page.fill('input[name="password"]', 'CorrectHorseBatteryStaple99!');
    await page.click('button[type="submit"]');

    // Confirm navigation into dashboard
    await expect(page.locator('h1')).toContainText('Account Overview');

    // Perform state-changing actions
    await page.goto('/settings/profile');
    await page.fill('input[name="displayName"]', 'Auditor Test User');
    await page.fill('input[name="billingEmail"]', '[email protected]');
    await page.click('button#save-profile');

    // Assert successful completion so ZAP logs the full mutation cycle
    await expect(page.locator('.toast-success')).toBeVisible();
  });
});

Every HTTP request triggered during this session, including authentication cookies, CSRF tokens, and JSON payloads, is now indexed in ZAP’s memory.

Step 4: Automate Vulnerability Analysis via the ZAP API

Once Playwright finishes execution, execute a coordinator script that interfaces with ZAP's REST API. This script checks passive scan progress, launches an active scan against recorded endpoints, and produces machine-readable vulnerability reports.

Here is a production-ready Node.js pipeline runner (run-dast-analysis.ts):

import axios from 'axios';
import fs from 'fs';
import path from 'path';

const ZAP_BASE_URL = process.env.ZAP_URL || 'http://127.0.0.1:8080';
const TARGET_URL = process.env.APP_URL || 'http://localhost:3000';

async function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function waitForPassiveScan(): Promise<void> {
  console.log('[DAST] Awaiting passive scan queue clearance...');
  let recordsRemaining = 1;

  while (recordsRemaining > 0) {
    const response = await axios.get(`${ZAP_BASE_URL}/JSON/pscan/view/recordsToScan/`);
    recordsRemaining = parseInt(response.data.recordsToScan, 10);
    if (recordsRemaining > 0) {
      console.log(`[DAST] Records remaining in queue: ${recordsRemaining}`);
      await sleep(2000);
    }
  }
  console.log('[DAST] Passive scanning complete.');
}

async function triggerActiveScan(): Promise<void> {
  console.log(`[DAST] Launching Active Scan against target: ${TARGET_URL}`);
  
  // Exclude administrative logout paths to keep context alive
  await axios.get(`${ZAP_BASE_URL}/JSON/ascan/action/excludeFromScan/`, {
    params: { regex: `${TARGET_URL}/logout.*` },
  });

  const scanInit = await axios.get(`${ZAP_BASE_URL}/JSON/ascan/action/scan/`, {
    params: {
      url: TARGET_URL,
      recurse: true,
      inScopeOnly: false,
    },
  });

  const scanId = scanInit.data.scan;
  let progress = 0;

  while (progress < 100) {
    await sleep(5000);
    const statusResponse = await axios.get(`${ZAP_BASE_URL}/JSON/ascan/view/status/`, {
      params: { scanId },
    });
    progress = parseInt(statusResponse.data.status, 10);
    console.log(`[DAST] Active Scan progress: ${progress}%`);
  }
  console.log('[DAST] Active Scan finalized.');
}

async function generateReports(): Promise<void> {
  console.log('[DAST] Fetching vulnerability alerts...');
  const alertsResponse = await axios.get(`${ZAP_BASE_URL}/JSON/core/view/alerts/`, {
    params: { baseurl: TARGET_URL },
  });

  const alerts = alertsResponse.data.alerts;
  const criticalOrHigh = alerts.filter(
    (a: { risk: string }) => a.risk === 'High' || a.risk === 'Critical'
  );

  const reportHtml = await axios.get(`${ZAP_BASE_URL}/OTHER/core/other/htmlreport/`);
  fs.writeFileSync(path.join(process.cwd(), 'zap-security-report.html'), reportHtml.data);

  console.log(`[DAST] Total alerts detected: ${alerts.length}`);
  console.log(`[DAST] High/Critical alerts detected: ${criticalOrHigh.length}`);

  if (criticalOrHigh.length > 0) {
    console.error('[DAST] Failing build due to security vulnerabilities above severity threshold.');
    process.exit(1);
  }
}

async function main(): Promise<void> {
  try {
    await waitForPassiveScan();
    if (process.env.RUN_ACTIVE_SCAN === 'true') {
      await triggerActiveScan();
    }
    await generateReports();
  } catch (error) {
    console.error('[DAST] Security scan orchestration failed:', error);
    process.exit(1);
  }
}

main();

Taming Active Scan Destruction: Mitigating Common Pitfalls

Active security scans do not simply observe; they aggressively fuzz endpoints. When applied carelessly, active scanning can cause severe side effects in downstream systems.

1. Database and State Pollution

An active scan targeting /api/v1/users/delete or /api/v1/organization/reset will execute those requests with varied parameter injections. If your application processes a DELETE request during a scan, it will destroy test fixtures required by subsequent requests.

Remediation:

  • Run active scans solely against ephemeral preview environments backed by dedicated seed databases that are destroyed immediately after pipeline completion.
  • Configure ZAP scan policies to exclude destructive paths. Use ZAP’s Regex Exclusion API to omit paths matching /.*delete.*/, /.*logout.*/, or /.*cancel.*/.

2. Session Invalidation During Attacks

If the active scanner hits an endpoint like /api/auth/logout, your recorded session cookie is terminated on the backend. Every subsequent attack request executed by ZAP will return 401 Unauthorized, rendering the remainder of the scan useless.

Remediation:

  • Configure explicit exclusion rules for authentication teardown URLs before invoking ascan/action/scan.
  • Alternatively, configure ZAP's Authentication Context engine with a re-authentication script to detect session invalidation and automatically refresh bearer tokens or cookies.

3. CI Job Timeouts and Scan Duration

Playwright tests may run in 3 to 5 minutes, but an unconstrained active scan firing thousands of attack vectors across hundreds of discovered endpoints can take several hours, exhausting CI runners.

Remediation:

  • Targeted Scan Policies: Do not run all scan rules. Create a custom ZAP scan policy (.policy file) that focuses exclusively on critical vulnerability classes: SQL injection, remote code execution, server-side template injection (SSTI), and path traversal. Disable slow checks like brute-force directory discovery.
  • Differentiate Passive and Active Pipelines: Execute passive scanning on every pull request (adding less than 30 seconds of overhead). Schedule full active scans to run nightly against a dedicated staging deployment.

When evaluating system integrity across complex distributed applications, teams should ensure testing protocols protect application data flow while preserving audit trails, similar to best practices for custom software supply chain security.


Hypothetical Calculation: CI/CD Cost and Coverage Trade-offs

To quantify the operational impact of integrating ZAP with Playwright, consider a hypothetical mid-sized B2B SaaS web application with the following parameters:

Application Scope:
- 120 unique API endpoints
- 45 interactive UI views
- 85 Playwright E2E test scenarios

Execution Times:
- Baseline Playwright E2E suite: 4 minutes, 15 seconds
- Passive ZAP analysis overhead: +25 seconds (total ~4m 40s)
- Targeted Active Scan duration (top 4 critical rule categories): 14 minutes, 30 seconds

In a team pushing 15 pull requests per day:

  • Running passive scanning on every PR adds less than 6.5 minutes of aggregate runner time across the entire day.
  • A nightly active scan consumes 14.5 minutes of off-peak compute, providing verified security telemetry across all 120 endpoints without human intervention.

Comparing this to an external pentest cycle illustrates the shift:

Evaluation VectorManual External PentestStandalone Crawler DASTPlaywright + ZAP Pipeline
Feedback Latency6 to 12 months1 to 7 daysPull Request / Nightly
Authentication ReliabilityHigh (Human guided)Low (Breaks on modern SPAs)High (Driven by Playwright)
Route CoverageHighLow (<25% of SPAs)Comprehensive (>90% of tested paths)
ReproducibilityLow (Text reports)MediumExact (Git commit tied to SARIF/HTML)

4 Best Practices for CI-Integrated DAST

Implementing automated security scanning successfully requires strict policies to prevent developer fatigue:

  1. Gate Pipelines on SARIF Exports with Definite Thresholds Configure ZAP to export results in Static Analysis Results Interchange Format (SARIF). Ingest this directly into GitHub Security Advisories or GitLab Vulnerability Management. Enforce strict exit gates: fail builds only on High or Critical alerts with Confidence > Medium to eliminate pipeline blocking on speculative false positives (such as generic missing header warnings).

  2. Decouple Dynamic Secrets from Test Code Ensure ZAP API tokens, test user credentials, and service URLs are injected via secure pipeline environment secrets. Never hardcode static ZAP API keys into version-controlled Playwright configurations or orchestration scripts.

  3. Maintain Context Isolation Across Test Roles If your application serves different privilege tiers (e.g., standard tenant vs. system administrator), run distinct Playwright contexts through separate ZAP sessions. This enables you to verify that parameter tampering across low-privileged user sessions cannot access endpoints discovered during high-privileged runs.

  4. Synchronize ZAP Policies with Application Tech Stack If your application runs strictly on Node.js and PostgreSQL, disable scan rules targeting PHP code injection, ASP.NET path disclosure, or Oracle-specific SQL payloads in your ZAP active scan policy. Pruning irrelevant rule sets slashes active scan times by up to 60% without sacrificing detection capabilities.


Moving Beyond Ephemeral Testing

Engineering teams often struggle to balance feature velocity with rigorous security posture. Building this level of test orchestration requires careful synchronization across frontend drivers, container networks, proxy engines, and vulnerability triage workflows.

Engineering organizations that want to build rock-solid, production-ready web platforms without diverting internal product teams from core delivery often partner with specialized teams. Engaging an experienced custom software development team ensures your applications are architected with automated verification, security hardening, and resilient CI/CD pipelines from day one.

Review your current test suite. If your Playwright tests already cover your critical business logic, you are one proxy configuration away from converting functional regression suites into an automated, continuous penetration testing engine.


Source: Software Testing: ZAP als Proxy: Regressionstests werden zu Sicherheitstests

Continue in this topic

Digital responsibility →