API Penetration Testing: Using ZAP Automation Framework [Practical Implementation]

Jaishree Patidar
6 min readMar 14, 2024

This article is a continuation of my previous blog. There have been a couple of changes made on the ZAP side, which are outlined below. Additionally, I’ll be providing an example of how to implement these changes using the OWASP Juice Shop application.

List of things updated by ZAP

  1. Docker images are changed. (Follow the link for more details)
  2. Add-on task is now deprecated. (Snapshot of ZAP documentation)

Prerequisite

Spinning up OWASP Juice Shop Application On Local

Using OWASP Juice Shop for practical implementation of ZAP Automation Framework

docker pull bkimminich/juice-shop
docker run -d -p 3000:3000 bkimminich/juice-shop

Open http://localhost:3000 to access the application.

Getting Started with ZAP Docker

Pull latest docker image (Reference for docker images of ZAP — https://www.zaproxy.org/blog/2023-06-13-ghcr-docker-images/)

docker pull softwaresecurityproject/zap-stable

Starting Implementation

Step 1: Import Open API specification

The ‘Openapi’ job in the ‘plan.yaml’ file retrieves a list of all APIs defined in the OpenAPI specification, either through a file or a URL.

In the ‘owasp_juiceshop_plan.yaml’ file provided below, the path to the swagger file is specified. Depending on the environment context defined in this plan, the corresponding URLs will be included.

Step 2: Authenticate

Adding authentication using HttpSender Script

Generating OWASP JuiceShop Application’s token and append it to all requests being send by ZAP to attack various API calls adding HttpSender script.

HTTP Sender — scripts that run against every request/response sent/received by ZAP. This includes the proxied messages, messages sent during active scan, fuzzer, …

In plan.yaml, I have added script automation job before active scan to add the httpSender script.

// The sendingRequest and responseReceived functions will be called for all requests/responses sent/received by ZAP, 
// including automated tools (e.g. active scanner, fuzzer, ...)

// Note that new HttpSender scripts will initially be disabled
// Right click the script in the Scripts tree and select "enable"

// 'initiator' is the component the initiated the request:
// 1 PROXY_INITIATOR
// 2 ACTIVE_SCANNER_INITIATOR
// 3 SPIDER_INITIATOR
// 4 FUZZER_INITIATOR
// 5 AUTHENTICATION_INITIATOR
// 6 MANUAL_REQUEST_INITIATOR
// 7 CHECK_FOR_UPDATES_INITIATOR
// 8 BEAN_SHELL_INITIATOR
// 9 ACCESS_CONTROL_SCANNER_INITIATOR
// 10 AJAX_SPIDER_INITIATOR
// For the latest list of values see the HttpSender class:
// https://github.com/zaproxy/zaproxy/blob/main/zap/src/main/java/org/parosproxy/paros/network/HttpSender.java
// 'helper' just has one method at the moment: helper.getHttpSender() which returns the HttpSender
// instance used to send the request.
//
// New requests can be made like this:
// msg2 = msg.cloneAll() // msg2 can then be safely changed as required without affecting msg
// helper.getHttpSender().sendAndReceive(msg2, false);
// print('msg2 response=' + msg2.getResponseHeader().getStatusCode())

var HttpRequestHeader = Java.type("org.parosproxy.paros.network.HttpRequestHeader");
var HtmlParameter = Java.type("org.parosproxy.paros.network.HtmlParameter");
var HtmlParameterType = Java.type("org.parosproxy.paros.network.HtmlParameter.Type");
var HttpMessage = Java.type("org.parosproxy.paros.network.HttpMessage");
var HttpHeader = Java.type("org.parosproxy.paros.network.HttpHeader");
var URI = Java.type("org.apache.commons.httpclient.URI");
var LastTokenGenerationTime;

function sendingRequest(msg, initiator, helper) {
var url = msg.getRequestHeader().getURI().toString();
if (url.contains('http://host.docker.internal:3000')) { // Adding token to requests containing following url
print('requestReceived called for url=' + url);
msg2 = msg.cloneAll()
requestUri = new URI('http://host.docker.internal:3000/rest/user/login', false);
requestHeader = new HttpRequestHeader(HttpRequestHeader.POST, requestUri, HttpHeader.HTTP10);

msg2.setRequestHeader(requestHeader);
msg2.getRequestBody().setBody("email=admin%40juice-sh.op&password=admin123")
var msgheader = msg2.getRequestHeader();
msgheader.addHeader("Content-Length", msg2.getRequestBody().length());
msg2.setRequestHeader(msgheader);

try {

helper.getHttpSender().sendAndReceive(msg2, true); //Generating token

print('msg2 request=' + msg2.getRequestHeader());
print('msg2 response status=' + msg2.getResponseHeader().getStatusCode())
print('msg2 response body=' + msg2.getResponseBody())
print('token ****' + JSON.parse(msg2.getResponseBody()).authentication.token)
//------------- Creating Get Token Request (Can update as per your token generation API call) -end ----------

org.zaproxy.zap.extension.script.ScriptVars.setGlobalVar("logintoken", JSON.parse(msg2.getResponseBody()).authentication.token);

//Adding authorization header to all calls made by ZAP

} catch (err) {
print('continue on error');
print(err.message);
}

var header = msg.getRequestHeader();
header.setHeader("Authorization", "Bearer " + org.zaproxy.zap.extension.script.ScriptVars.getGlobalVar("logintoken"));
msg.setRequestHeader(header);

print('msg request header=' + msg.getRequestHeader())
}
}

function responseReceived(msg, initiator, helper) {
// Debugging can be done using println like this
// print('responseReceived called for url=' + msg.getRequestHeader().getURI().toString())
var url = msg.getRequestHeader().getURI().toString();
if (url.contains('http://host.docker.internal:3000')) { // Adding token to requests containing following url
print('responseReceived called for url=' + msg.getRequestHeader().getURI().toString())
print('responseReceived called response status = ' + msg.getResponseHeader().getStatusCode())
}
}

Step 3: Passing Request URL Parameters

Parameter Passing using Replacer

There are 2 ways this can be achieved using Automation Framework:

  1. Using Options.prop file
replacer.full_list(0).description=requestHeader1
replacer.full_list(0).enabled=true
replacer.full_list(0).matchtype=REQ_HEADER_STR
replacer.full_list(0).matchstr=Products/1
replacer.full_list(0).regex=false
replacer.full_list(0).replacement=Products/99

2. Task in Plan file

- rules:
- description: "header1"
url: ""
matchType: "req_header_str"
matchString: "Products/1"
matchRegex: false
replacementString: "Products/99"
tokenProcessing: false
parameters:
deleteAllRules: false
name: "replacer"
type: "replacer"

This will update all “Products/1” calls with “Products/99” like below:

This feature proves particularly valuable when your API request URL includes a UUID for accessing a specific resource. If an invalid UUID is provided, the API will consistently return a 404 error, making it impossible to identify vulnerabilities through various attack to this endpoint.

Step 4: Active Scan

Running scan command to execute the plan file using docker image:

docker container run --platform linux/arm64 -v $(pwd):/zap/wrk/:rw -t softwaresecurityproject/zap-stable bash -c "zap.sh -cmd -autorun /zap/wrk/plans/owasp_juiceshop_plan.yaml"

Step 5: Report Generation

Added 2 automation jobs in plan.yaml to generate two type of reports:

A. HTML Report — Easier to understand with various links with context about the potential risk.

B. XML Report — Which can be used to fail the pipeline, if a risk is found excluding false positives.

Note: When publishing HTML reports in the pipeline as an artifact, ensure to exclude any bearer token printed within the report, especially if your access token remains valid for an extended period.

ZAP Automation Framework Plan File

File containing all jobs to be performed in sequential order: (owasp_juiceshop_plan.yaml)

---
env:
contexts:
- name: "juiceShopContext"
urls:
- "http://host.docker.internal:3000/"
includePaths:
- "http://host.docker.internal:3000/.*"
excludePaths: []
authentication:
parameters: {}
verification:
method: "response"
pollFrequency: 60
pollUnits: "requests"
sessionManagement:
method: "cookie"
parameters: {}
parameters:
failOnError: true
failOnWarning: false
progressToStdout: true
jobs:
- rules:
- description: "header1"
url: ""
matchType: "req_header_str"
matchString: "Products/1"
matchRegex: false
replacementString: "Products/99"
tokenProcessing: false
parameters:
deleteAllRules: false
name: "replacer"
type: "replacer"
- type: alertFilter
alertFilters:
- ruleId: 10021
ruleName: "X-Content-Type-Options Header Missing"
newRisk: "False Positive"
url: "http://host.docker.internal:3000/.*"
urlRegex: true
- type: openapi
parameters:
apiFile: "/zap/wrk/openapi-specs/swagger-juiceshop_demo.json"
targetUrl: "http://host.docker.internal:3000/"
- type: script
parameters:
action: "add"
type: "httpsender"
engine: "Oracle Nashorn"
name: "GetToken"
file: "/zap/wrk/scripts/GetToken_new.js"
- name: "activeScan"
type: "activeScan"
policyDefinition:
rules: []
- parameters:
template: "risk-confidence-html"
theme: "original"
reportDir: "../reports/"
reportFile: "juiceShopHtmlReport"
reportTitle: "ZAP Scanning Report"
reportDescription: ""
displayReport: false
risks:
- "info"
- "low"
- "medium"
- "high"
confidences:
- "falsepositive"
- "low"
- "medium"
- "high"
- "confirmed"
sections:
- "siteRiskCounts"
- "responseBody"
- "appendix"
- "alertTypes"
- "responseHeader"
- "alertTypeCounts"
- "riskConfidenceCounts"
- "alerts"
- "aboutThisReport"
- "contents"
- "requestBody"
- "reportDescription"
- "reportParameters"
- "requestHeader"
- "summaries"
sites: []
name: "report"
type: "report"
- parameters:
template: "traditional-xml"
reportDir: "../reports/"
reportFile: "juiceShopXmlReport"
reportTitle: "ZAP Scanning Report"
reportDescription: ""
displayReport: false
risks:
- "info"
- "low"
- "medium"
- "high"
confidences:
- "falsepositive"
- "low"
- "medium"
- "high"
- "confirmed"
sites: []
name: "report"
type: "report"

Tips & Tricks:

  1. If you’re using a Mac machine with Colima and your execution abruptly terminates with error code 0, initiate Colima with increased RAM and memory allocation using the command provided below.
colima start --cpu 4 --memory 8

2. Adding log messages in your HttpSender script will help you better debug in case of authorisation failures.

3. Before publishing HTML reports as artefact in a pipeline, ensure to remove any access tokens.

4. Depending on the token’s validity period, you can choose to pass it from an external source or create HTTP Sender script to generate it at an interval or before each API call.

You can find the complete code at location — ZAP-APIScan-AutomationFramework

--

--