Web Security (OWASP Top 10:2025) Module 15 – Security testing and OWASP ZAP in CI
ZAP API scans, authenticated scans and triage
Run a ZAP API scan from an OpenAPI description, write an Automation Framework plan, scan signed-in areas with a safe test user, and triage each alert.
What you will learn
- Run an API scan from an OpenAPI description with ZAP's script or an Automation Framework plan
- Write an Automation Framework plan with an environment, bounded jobs, a report and an exit status, and run it in CI
- Configure an authenticated scan with a dedicated test user, a safely handled session token and excluded risky paths
- Classify alerts as true or false positives, map each to a CWE and a Top 10 category, and fix it with a regression test
- Limit scans to a copy you own, with disposable data, bounded minutes and threads, and a reviewed suppression file
Before you start
What you’ll learn · Before you start
What you will learn
- Run an API scan from an OpenAPI description with ZAP's script or an Automation Framework plan
- Write an Automation Framework plan with an environment, bounded jobs, a report and an exit status, and run it in CI
- Configure an authenticated scan with a dedicated test user, a safely handled session token and excluded risky paths
- Classify alerts as true or false positives, map each to a CWE and a Top 10 category, and fix it with a regression test
- Limit scans to a copy you own, with disposable data, bounded minutes and threads, and a reviewed suppression file
Before you start
On this page
An active scan of the training shop’s vulnerable build raised three High alerts: a SQL injection on one parameter, and cross-site scripting on another, reported twice. The same plan on the fixed build raised none. It also said nothing about the worst flaw in the shop, one customer reading another’s order, which a test with two users finds at once.
You will set up ZAP API scans from a description, write an Automation Framework plan that fails a build on the right alerts, run authenticated scans with a test user that can do no harm, and triage each alert into a fix and a test.
An active scan finds what a payload can show, and cannot know whose data is whose
ZAP API scans read a description of an API in OpenAPI, SOAP or GraphQL form, import its endpoints and run an active scan tuned to APIs. Unlike the baseline, an active scan attacks: it sends requests built to provoke a flaw to every parameter it knows. It runs only against a copy you own, with disposable data, and every run needs bounds on its time and its threads. ZAP API scans take a rules file of the same format and end with the same exit codes as the baseline. Authenticated scans reach the signed-in part of an application, where most of the data is, and they do so only when ZAP carries a session. Authenticated scans are also where an active scan can do the most harm, because the attacks reach endpoints that create, change and delete data.
Turning the alerts of a scan into decisions: each alert is reproduced, classified, mapped to a CWE, fixed and given a regression test. is what comes after the scan: each alert is reproduced, classified as a true or false positive, mapped to a CWE and a Top 10 category, fixed, and given a regression test. The program summarises three recorded active scans of ShopNest, the training shop, by risk, to show how much there is to triage. They ran a plan with ZAP’s default policy and a visit to the search page, so they include the cross-site scripting rules that the API scan script leaves out:
// The alerts of three active scans of ShopNest by ZAP 2.17.0, one line for each rule: the id, the name, the risk and the CWE, and for an alert that an
// attack raised, the endpoint and the parameter. The scans ran an Automation Framework plan whose last job, exitStatus, had errorLevel High and warnLevel
// Medium. The program works out the plan's exit value from the alerts, and prints the exit value that each recorded run ended with beside it.
const INFO = [
['10104', 'User Agent Fuzzer', 'Informational', 0],
['10111', 'Authentication Request Identified', 'Informational', -1],
['10112', 'Session Management Response Identified', 'Informational', -1],
];
const BUILDS = {
vulnerable: {
recorded: 1,
alerts: [
['40018', 'SQL Injection', 'High', 89, 'GET /api/products', 'category'],
['40012', 'Cross Site Scripting (Reflected)', 'High', 79, 'GET /search', 'q'],
['40026', 'Cross Site Scripting (DOM Based)', 'High', 79, 'GET /search', 'q'],
['10038', 'Content Security Policy (CSP) Header Not Set', 'Medium', 693],
['10020', 'Missing Anti-clickjacking Header', 'Medium', 1021],
['10202', 'Absence of Anti-CSRF Tokens', 'Medium', 352],
['90003', 'Sub Resource Integrity Attribute Missing', 'Medium', 345],
['10010', 'Cookie No HttpOnly Flag', 'Low', 1004],
['10054', 'Cookie without SameSite Attribute', 'Low', 1275],
['10017', 'Cross-Domain JavaScript Source File Inclusion', 'Low', 829],
['10021', 'X-Content-Type-Options Header Missing', 'Low', 693],
['10037', 'Server Leaks Information via "X-Powered-By" HTTP Response Header Field(s)', 'Low', 497],
...INFO,
],
},
fixed: { recorded: 0, alerts: INFO },
'fixed, CSP header removed': { recorded: 2, alerts: [['10038', 'Content Security Policy (CSP) Header Not Set', 'Medium', 693], ...INFO] },
};
const RANK = { Informational: 0, Low: 1, Medium: 2, High: 3 };
const AUTHORISATION = [284, 285, 639, 862, 863]; // CWEs about who may read or change what
const exitValue = (alerts) => {
const top = Math.max(0, ...alerts.map((a) => RANK[a[2]]));
return top >= RANK.High ? 1 : top >= RANK.Medium ? 2 : 0; // errors give 1, warnings give 2, a clean plan gives 0
};
console.log('build'.padEnd(27) + 'High Medium Low Info alerts plan exit');
for (const [name, build] of Object.entries(BUILDS)) {
const count = (risk) => build.alerts.filter((a) => a[2] === risk).length;
const exit = exitValue(build.alerts);
console.log(`${name.padEnd(27)}${String(count('High')).padStart(4)}${String(count('Medium')).padStart(8)}${String(count('Low')).padStart(5)}${String(count('Informational')).padStart(6)}${String(build.alerts.length).padStart(8)} ${exit} (the run ended with ${build.recorded})`);
}
console.log('\nHigh alerts of the vulnerable build:');
for (const [id, name, , cwe, where, param] of BUILDS.vulnerable.alerts.filter((a) => a[2] === 'High')) console.log(` ${id} ${name}, CWE-${cwe}: ${where}, parameter ${param}`);
const vulnerable = BUILDS.vulnerable.alerts;
console.log(`\nAlerts about who may read what (CWE ${AUTHORISATION.join(', ')}): ${vulnerable.filter((a) => AUTHORISATION.includes(a[3])).length} of ${vulnerable.length}`); Output
build High Medium Low Info alerts plan exit vulnerable 3 4 5 3 15 1 (the run ended with 1) fixed 0 0 0 3 3 0 (the run ended with 0) fixed, CSP header removed 0 1 0 3 4 2 (the run ended with 2) High alerts of the vulnerable build: 40018 SQL Injection, CWE-89: GET /api/products, parameter category 40012 Cross Site Scripting (Reflected), CWE-79: GET /search, parameter q 40026 Cross Site Scripting (DOM Based), CWE-79: GET /search, parameter q Alerts about who may read what (CWE 284, 285, 639, 862, 863): 0 of 15
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node active_summary.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The vulnerable build raised 15 alerts: three High, four Medium, five Low and three informational. The High alerts are a SQL injection on the category parameter of the products endpoint, and cross-site scripting on the q parameter of the search page, reported as reflected by one rule and as DOM based by another, because one echo of the query string trips both. The plan ended with exit value 1, the fixed build with 0, and the fixed build with one header taken away with 2. None of the 15 alerts is about who may read what. The order endpoint of the vulnerable build gives any signed-in user any customer’s order, and the scan has no way to know whose order was whose.
Key idea
A scan finds what a payload can show. Write tests for the rules that only your application knows.
Checkpoint
Check: What an active scan finds
Flashcards · 4 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
How does a ZAP active scan differ from the baseline scan?
An active scan attacks: it sends requests built to provoke a flaw to every parameter it knows, so it runs only against a copy you own, with disposable data and bounds on time and threads. The baseline sends no attack. ZAP API scans import an OpenAPI, SOAP or GraphQL description and take a rules file of the same format, with the same exit codes.
-
Why do authenticated scans matter, and why are they riskier?
They reach the signed-in part of an application, where most of the data is, when ZAP carries a session. They are also where an active scan can do the most harm, because the attacks reach endpoints that create, change and delete data, so use a test user whose actions can do no damage, on a disposable copy.
-
What is triage after a scan?
Turning the alerts into decisions: each alert is reproduced, classified as a true or false positive, mapped to a CWE and to a Top 10 category, fixed, and given a regression test.
-
What did the active scan of the vulnerable shop find, and what could it not know?
15 alerts: three High (a SQL injection on the category parameter, and cross-site scripting on the q parameter of the search page, reported twice, as reflected and as DOM based), four Medium, five Low and three informational. None was about who may read what: the order endpoint gave any signed-in user any customer's order, which a test with two users finds at once. A scan finds what a payload can show; write tests for the rules that only your application knows.
An Automation Framework plan runs the scan the same way every time Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- A signed-in scan needs a test user that can do no harm
- A scan cannot tell whose data is whose, so test that yourself
- Triage turns a report into a short queue of fixes
- Gate on new alerts, and keep suppressions honest
- Apply a suppression file to the alerts of a scan
- Practise: from the first scan to the gate
- Interview questions
- You are asked to run an authenticated active ZAP scan of the staging copy of an e-commerce API every night. What do you set up so that it finds problems and does no harm?
- A first active scan of your service raises a hundred alerts and the team lead wants a plan. How do you turn them into work, and how do you decide what to fix first?
- A penetration test finds that signed-in users can read each other's orders, and your nightly ZAP scans never reported it. What went wrong, and what do you change in the pipeline?
- Your gate fails on new alerts of Medium risk and above. A team asks to suppress one that they call a false positive. What do you require, and who decides?
- Key takeaways
2 diagrams · 4 runnable examples (JavaScript, JavaScript (plan_check.mjs), JavaScript (plans.mjs), JavaScript (reports.mjs), JavaScript (zap_report_triage.mjs), Python, YAML (zap-api-plan.yaml), YAML (zap-api.yml)) · 1 exercise with sample tests · 4 interview answers · about 47 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Terms in this lesson
- accepted risk
- A real weakness that a person with the authority has decided to carry for now. The decision is written down with a reason, an owner and an expiry, and the alert stays on the list. A High risk is never accepted by a configuration file.
- Automation Framework
- ZAP's way of running a scan from one YAML plan: an environment that says what the plan may touch, and jobs that run in order, such as a spider, an active scan, a report and an exit status. The plan ends with an exit value of 0, 1 or 2.
- context
- In ZAP, the set of addresses that belong to the application under test, defined by the starting addresses and by include and exclude patterns. Jobs that name a context stay inside it, so it is the first limit on where a scan may go.
- report diff
- A comparison of two scan reports that lists the alerts that are new, resolved or unchanged. Alerts are keyed by rule and parameter, not by page, because a crawl reaches different pages on each run. A gate that fails on new alerts leaves old ones to the plan.
- triage
- Turning the alerts of a scan into decisions: reproduce each one, classify it as a true positive, a false positive or an accepted risk, merge alerts that share a root cause, map it to a CWE, fix it with a regression test, and suppress only what remains false.
References
- ZAP API scan (ZAP project)
- ZAP Automation Framework (ZAP project)
- Automation Framework, the exitStatus job (ZAP project)
- Automation Framework, the activeScan job (ZAP project)
- Handling authentication yourself in automation (ZAP project)
- zaproxy/action-af (ZAP project)
- OWASP Web Security Testing Guide (OWASP Foundation)
- A01:2025 Broken Access Control (OWASP Foundation)
- A05:2025 Injection (OWASP Foundation)
- A06:2025 Insecure Design (OWASP Foundation)
- CWE-89, Improper Neutralization of Special Elements used in an SQL Command (MITRE)
- Secure use reference for GitHub Actions (GitHub Docs)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress