DAST Testing Guide: OWASP ZAP, Burp Suite, and Automated Scanning
A practical guide to Dynamic Application Security Testing — differences from SAST and IAST, running ZAP in CI/CD, Burp Suite for manual testing, authenticated scanning, and integrating results.
Dynamic Application Security Testing (DAST) attacks a running application the same way an attacker would — by sending malformed inputs, observing responses, and inferring vulnerabilities from behavior. Unlike SAST, which reads source code, DAST needs nothing but a URL. That makes it language-agnostic and effective at finding issues that only appear at runtime.
DAST vs SAST vs IAST
Understanding the three categories prevents overlap and gaps in your testing strategy:
SAST (Static) reads source code or compiled artifacts before execution. It's fast to integrate into CI, catches code-level issues early, but can't see runtime configuration problems, business logic bugs, or anything that depends on real HTTP behavior.
DAST (Dynamic) sends HTTP requests to a running application and observes responses. It's inherently accurate about what's actually exploitable because it tests the real system. It's blind to code structure and can only reach what's accessible over the network.
IAST (Interactive) instruments the application at runtime — agents inside the JVM, .NET CLR, or Node.js process observe code execution as traffic flows through. IAST combines the accuracy of DAST (tests real behavior) with the depth of SAST (sees exactly which code path was exercised). The downside is language-specific agents, overhead in production, and significant licensing costs.
For most teams, SAST + DAST gives 80% of the value at 20% of the cost of IAST.
OWASP ZAP in CI/CD
OWASP ZAP (Zed Attack Proxy) is the most widely used open-source DAST tool. It has a GUI for manual testing, a headless mode for automation, and a REST API for CI integration.
The ZAP Docker image is the easiest CI integration path:
# .github/workflows/dast.yml
name: DAST Scan
on:
push:
branches: [main]
schedule:
- cron: '0 2 * * *' # nightly
jobs:
zap-scan:
runs-on: ubuntu-latest
services:
app:
image: myapp:${{ github.sha }}
ports:
- 8080:8080
steps:
- name: ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.12.0
with:
target: 'http://localhost:8080'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a' # include alpha rules
- name: Upload ZAP Report
uses: actions/upload-artifact@v4
if: always()
with:
name: zap-report
path: report_html.html
ZAP has three main scan modes:
Baseline scan: Passive only. ZAP spiders the site and reports issues found without sending attack payloads. Fast (~2 minutes), safe enough to run against production. Catches missing headers, information disclosure, obvious misconfigurations.
Full scan: Active attack mode. ZAP attempts actual exploits. Takes 15-60 minutes, should only run against non-production environments. Finds injection vulnerabilities, XSS, path traversal.
API scan: Targets OpenAPI/Swagger specifications directly. Better coverage for REST APIs since it knows the full endpoint surface without needing to spider.
# API scan using OpenAPI spec
docker run --network host \
-v $(pwd):/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-api-scan.py \
-t /zap/wrk/openapi.json \
-f openapi \
-r api-scan-report.html \
-J api-scan-report.json \
-x api-scan-report.xml
Burp Suite Pro for Manual Testing
OWASP ZAP handles automated scanning well, but Burp Suite Professional is the industry standard for manual security testing and penetration testing.
Burp operates as a proxy between your browser and the target. Every request flows through Burp, where you can inspect, modify, replay, and attack it.
Key Burp features for web application security testing:
Intruder enables fuzzing attack payloads against specific parameter positions. You mark injection points and Burp iterates through wordlists.
Repeater lets you manually modify and replay individual requests. Essential for confirming vulnerabilities and understanding their exact conditions.
Scanner (Pro only) automates active scanning against the entire request history captured during manual browsing. Unlike ZAP's spider, Burp Scanner benefits from the authenticated session your browser already has.
Collaborator provides out-of-band detection. When testing for blind SSRF, blind XSS, or blind SQL injection, payloads cause the target to make DNS lookups or HTTP requests to Burp's external server, confirming exploitation even when the vulnerability produces no visible output.
# Burp Suite extension (BApp) example using Python Montoya API
from burp import IBurpExtender
from burp import IHttpListener
class BurpExtender(IBurpExtender, IHttpListener):
def registerExtenderCallbacks(self, callbacks):
self._callbacks = callbacks
self._helpers = callbacks.getHelpers()
callbacks.registerHttpListener(self)
callbacks.setExtensionName("Custom Security Header Checker")
def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):
if not messageIsRequest:
response = messageInfo.getResponse()
analyzed = self._helpers.analyzeResponse(response)
headers = analyzed.getHeaders()
header_names = [h.split(":")[0].lower() for h in headers]
required = ["x-frame-options", "content-security-policy",
"x-content-type-options", "strict-transport-security"]
for req_header in required:
if req_header not in header_names:
print(f"[MISSING HEADER] {req_header} not present in response")
Authenticated Scanning
Most web applications require authentication, and DAST tools must be able to log in to test protected functionality. This is the hardest part of DAST automation.
ZAP Authenticated Scanning
ZAP supports several authentication mechanisms:
# ZAP Python API: set up form-based authentication
import zapv2
zap = zapv2.ZAPv2(apikey='your-api-key', proxies={'http': 'http://127.0.0.1:8080'})
# Create context and configure auth
context_id = zap.context.new_context('authenticated-app')
auth_url = 'https://app.example.com/login'
# Set form-based auth
zap.authentication.set_authentication_method(
contextid=context_id,
authmethodname='formBasedAuthentication',
authmethodconfigparams=f'loginUrl={auth_url}&loginRequestData=email%3D%7B%25username%25%7D%26password%3D%7B%25password%25%7D'
)
# Define logged-in indicator (regex on response)
zap.authentication.set_logged_in_indicator(
contextid=context_id,
loggedinindicatorregex='\\Qsign_out\\E'
)
# Set credentials
user_id = zap.users.new_user(contextid=context_id, name='test-user')
zap.users.set_authentication_credentials(
contextid=context_id,
userid=user_id,
authcredentialsconfigparams='username=test@example.com&password=testpassword123'
)
# Enable user and start spider
zap.users.set_user_enabled(contextid=context_id, userid=user_id, enabled='true')
zap.spider.scan_as_user(contextid=context_id, userid=user_id, url=auth_url)
For token-based APIs (JWT), inject the token directly:
# GitHub Actions: ZAP API scan with Bearer token
- name: ZAP Authenticated API Scan
run: |
docker run --network host \
-v ${{ github.workspace }}:/zap/wrk/:rw \
ghcr.io/zaproxy/zaproxy:stable \
zap-api-scan.py \
-t /zap/wrk/openapi.json \
-f openapi \
-r /zap/wrk/report.html \
-z "-config replacer.full_list(0).description=auth-token \
-config replacer.full_list(0).enabled=true \
-config replacer.full_list(0).matchtype=REQ_HEADER \
-config replacer.full_list(0).matchstr=Authorization \
-config replacer.full_list(0).replacement=Bearer\ ${{ secrets.TEST_JWT_TOKEN }}"
Handling False Positives
DAST tools generate false positives, but fewer than SAST tools because they're testing real behavior. Common false positive categories:
Reflected parameter in non-HTML context: ZAP may flag reflected input in a JSON response as XSS even though it's not rendered as HTML. Review the Content-Type header.
CSRF false positives: Some CSRF scanners incorrectly flag APIs that use custom headers or token-based auth (which is CSRF-safe by design).
SQL error pattern matching: ZAP detects SQL injection by looking for database error strings in responses. Custom error pages that contain the word "SQL" in explanatory text can trigger this.
Create a ZAP rules configuration file to suppress known false positives:
# .zap/rules.tsv
# Format: rule_id TAB action (IGNORE/WARN/FAIL) TAB parameter
10096 IGNORE # Timestamp disclosure - irrelevant for our app
10027 WARN # Information disclosure - verbose messages
0 IGNORE # All informational alerts
Integrating DAST Results into Issue Tracking
Raw DAST output needs to flow into your vulnerability management process. SARIF format is the standard for integration with GitHub Code Scanning:
# ZAP output in SARIF format
docker run ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py \
-t https://staging.example.com \
-x zap-results.xml \
-J zap-results.json
# Convert to SARIF with zap-to-sarif
npx @security-alert/zap-to-sarif zap-results.xml > zap-results.sarif
# Upload to GitHub Code Scanning
- name: Upload SARIF to GitHub
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: zap-results.sarif
category: dast
For Jira integration, map severity to priority and deduplicate by rule ID + URL:
import json
import hashlib
from jira import JIRA
def create_dast_tickets(sarif_file: str, jira_project: str):
with open(sarif_file) as f:
sarif = json.load(f)
jira = JIRA(server='https://yourcompany.atlassian.net',
basic_auth=('user@company.com', 'api-token'))
for run in sarif['runs']:
for result in run['results']:
rule_id = result['ruleId']
message = result['message']['text']
uri = result['locations'][0]['physicalLocation']['artifactLocation']['uri']
# Deduplicate
dedup_key = hashlib.sha256(f"{rule_id}:{uri}".encode()).hexdigest()[:12]
severity_map = {
'error': 'High',
'warning': 'Medium',
'note': 'Low'
}
priority = severity_map.get(result['level'], 'Low')
jira.create_issue(
project=jira_project,
summary=f"[DAST] {rule_id}: {uri[:60]}",
description=f"**Finding**: {message}\n\n**URL**: {uri}\n\n**Dedup Key**: {dedup_key}",
issuetype={'name': 'Security Vulnerability'},
priority={'name': priority},
labels=['dast', 'automated']
)
Running a DAST Program That Works
The biggest DAST failure mode is running scans but not acting on results. A few practices help:
Separate baseline from full scans. Baseline scans (passive only) can run on every PR. Full active scans should run nightly against staging. Confusing these results in either blocking deploys with noisy passive results or never running active tests.
Version your attack surface. Track which endpoints exist so you notice when protected endpoints become unauthenticated, or when new endpoints appear without security testing.
Triage is ongoing, not one-time. Assign DAST findings to specific teams based on the URL path. Unowned findings go stale. Ownership prevents that.
Test all authentication states. Run scans as unauthenticated, regular user, and admin. Horizontal privilege escalation (user A accessing user B's resources) is only found when the scanner is authenticated as user A and probes user B's identifiers.