Dynamic Application Security Testing (DAST) สำหรับ REST API ด้วย OWASP ZAP

Nont Banditwong
6 min readOct 7, 2024

--

OWASP ZAP Logo

OWASP ZAP (Zed Attack Proxy) เป็นเครื่องมือทดสอบความปลอดภัยเว็บแอปพลิเคชันแบบโอเพ่นซอร์ส ที่สามารถใช้ทดสอบ API ได้ โดย ZAP จะทำการตรวจสอบช่องโหว่ต่างๆ ใน API คล้ายกับการทดสอบเว็บแอปพลิเคชันทั่วไป แต่จะเน้นไปที่ประเด็นเฉพาะของ API เช่น การตรวจสอบ input validation, authorization, และการจัดการข้อมูลที่ละเอียดอ่อน

ในการทดสอบจะใช้ API Definition ในรูปแบบ OpenAPI Swagger หรือ WSDL เพื่อให้ ZAP เข้าใจโครงสร้างของ API ได้ง่ายขึ้น

ในตัวอย่างที่จะเขียนถึงนี้ตั้งใจจะนำ ZAP ไปในใน DAST ภายใน CI/CD Pipeline หลังจาก Deploy application ไปที่ Development หรือ UAT environment ดังนั้นจะใช้งาน ZAP ในรูปแบบของคอนเทนเนอร์ เพื่อให้สอดคล้องกับการใช้งานใน Pipeline ที่ใช้งานอยู่เช่น Bitbucket Pipeline ต่อไป

ในกรณีทั่วๆไปสิ่งที่ต้องเตรียมคือ

  • แอปพลิเคชันที่ถูก deploy และพร้อมใช้งาน
  • OpenAPI Specification ที่มี url ชี้ไปที่แอปพลิเคชันในข้อ 1.
  • คอนเทนเนอร์อิมเมจ ของ OWASP ZAP

ในบทความนี้เพื่อแสดงให้เห็นวิธีการใช้งาน ZAP คร่าวๆจะใช้คอนเทนเนอร์ ของ โปรเจค spring-petclinic-rest ในการทดสอบ โดยจะ deploy ลงไปใน Docker desktop และตั้งชื่อคอนเทนเนอร์ว่า spring-petclinic-rest ส่วนของ OWASP ZAP ก็จะทำงานภายใต้ Docker desktop เช่นเดียวกัน โดย จะทำการ scan REST API ไปที่ service ชื่อ spring-petclinic-rest ผ่านคอนเทนเนอร์ network ที่ชื่อ dast

  1. Download OpenAPI Specification สำหรับ spring-petclinic-rest
curl -LO https://raw.githubusercontent.com/spring-petclinic/spring-petclinic-rest/refs/heads/master/src/main/resources/openapi.yml

2. แก้ไขไฟล์ openapi.yml ที่ Download มาโดยเปลี่ยนข้อมูลใน url จาก http://localhost:9966/petclinic/api เป็น http://spring-petclinic-rest:9966/petclinic/api เพื่อให้ ZAP สามารถ scan ไปที่ url ของ spring-petclinic-rest ที่ถูก deploy ใน Docker Desktop

ตัวอย่างบางส่วนของไฟล์ openapi.yml ที่ถูกแก้ไขค่าแล้ว

openapi: 3.0.1
info:
title: Spring PetClinic
description: Spring PetClinic Sample Application.
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0
version: '1.0'
servers:
- url: http://spring-petclinic-rest:9966/petclinic/api
tags:
- name: failing
description: Endpoint which always returns an error.
...

3. สร้างคอนเทนเนอร์ network ชื่อ dast ด้วยคำสั่ง

docker network create dast

4. Run คอนเทนเนอร์ ของแอปพลิเคชัน spring-petclinic-rest

docker run --rm --name spring-petclinic-rest --network dast -p 9966:9966 springcommunity/spring-petclinic-rest

คำสั่งนี้ใช้สำหรับรันแอปพลิเคชัน Spring Petclinic REST ภายในคอนเทนเนอร์โดยมีรายละเอียดดังนี้:

  • docker run: คำสั่งพื้นฐานสำหรับรันคอนเทนเนอร์
  • --rm: ลบคอนเทนเนอร์โดยอัตโนมัติหลังจากหยุดทำงาน
  • --name spring-petclinic-rest: ตั้งชื่อคอนเทนเนอร์เป็น "spring-petclinic-rest" เพื่อให้ง่ายต่อการอ้างอิง
  • --network dast: เชื่อมต่อคอนเทนเนอร์เข้ากับ continer network ที่ชื่อ "dast"
  • -p 9966:9966: แมปพอร์ต 9966 ของโฮสต์เครื่อง กับพอร์ต 9966 ของคอนเทนเนอร์ ทำให้สามารถเข้าถึงแอปพลิเคชันจากภายนอกคอนเทนเนอร์ได้ผ่านพอร์ต 9966
  • springcommunity/spring-petclinic-rest: คอนเทนเนอร์อิมเมจ ที่ใช้ ซึ่งในที่นี้คืออิมเมจ "springcommunity/spring-petclinic-rest" ซึ่งเป็นอิมเมจของแอปพลิเคชัน Spring Petclinic REST

5. ใช้ OWASP ZAP เพื่อ Scan API ของ แอปพลิเคชัน Spring Petclinic REST

docker run --rm --network dast -v $(pwd):/zap/wrk/:rw -t ghcr.io/zaproxy/zaproxy:stable zap-api-scan.py -f openapi -t openapi.yml -r report.html -x report.xml

คำสั่ง Docker นี้ใช้สำหรับรัน OWASP ZAP ภายในคอนเทนเนอร์ เพื่อทำการทดสอบ API โดยอัตโนมัติผ่านไฟล์ OpenAPI definition โดยมีรายละเอียดดังนี้:

  • docker run: คำสั่งพื้นฐานสำหรับรันคอนเทนเนอร์
  • --rm: ลบคอนเทนเนอร์โดยอัตโนมัติหลังจากหยุดทำงาน
  • --network dast: เชื่อมต่อคอนเทนเนอร์เข้ากับคอนเทนเนอร์ network ที่ชื่อ "dast"
  • -v $(pwd):/zap/wrk/:rw: Mount ไดเรกทอรีปัจจุบัน ($(pwd)) ของโฮสต์เครื่อง ไปยัง /zap/wrk/ ภายในคอนเทนเนอร์ โดยอนุญาตให้ read และ write (rw) เพื่อให้สามารถแชร์ไฟล์ระหว่างโฮสต์เครื่อง และคอนเทนเนอร์ได้ เช่น ไฟล์ openapi.yml, report.html, และ report.xml
  • -t ghcr.io/zaproxy/zaproxy:stable: ระบุคอนเทนเนอร์อิมเมจ ที่จะใช้ ซึ่งในที่นี้คืออิมเมจ "ghcr.io/zaproxy/zaproxy:stable" ซึ่งเป็นอิมเมจ stable ล่าสุดของ OWASP ZAP
  • zap-api-scan.py: รันสคริปต์ zap-api-scan.py ภายในคอนเทนเนอร์ ซึ่งเป็นสคริปต์สำหรับการทดสอบ API โดยอัตโนมัติ โดยใช้ ZAP
  • -f openapi: ระบุรูปแบบของ API definition เป็น OpenAPI
  • -t openapi.yml: ระบุตำแหน่งของไฟล์ OpenAPI definition (openapi.yml) ซึ่งถูก mount เข้าไปในคอนเทนเนอร์แล้ว
  • -r report.html: สร้างรายงานผลการทดสอบในรูปแบบ HTML ชื่อ report.html
  • -x report.xml: สร้างรายงานผลการทดสอบในรูปแบบ XML ชื่อ report.xml

เมื่อ ZAP ทำการ Scan เสร็จแล้วจะได้ output เป็นไฟล์ report.html และ report.xml

HTML Report (report.html) แสดงผลการทดสอบตามรูป

OWASP ZAP HTML Report

ในกรณีที่เราใช้ใน CI/CD Pipeline เราต้องแปลง XML report ให้อยู่ในรูปแบบที่ CI/CD Platform เข้าใจ เช่นในกรณีของ Bitbucket Pipeline จะเข้าใจ Report ในรูปแบบ JUnit XML ซึ่งเราสามารถแปลงได้โดยใช้ XSLT

ในการทดสอบใช้ไฟล์ XSL จากโครงการ zap2junit

Download ไฟล์ zap2junit.xsl

curl -LO https://raw.githubusercontent.com/coderpatros/zap2junit/refs/heads/master/zap2junit.xsl

ติดตั้ง saxon ถ้าใน CI/CD Pipeline เราต้องเตรียม custom คอนเทนเนอร์อิมเมจ ซึ่งรวม saxon และ OWASP ZAP ในคอนเทนเนอร์อิมเมจเดียวกัน แต่เพื่อความง่ายในตัวอย่างนี้จะแปลงไฟล์ใน macOS ซึ่งสามารถติดตั้ง saxon ด้วย brew

brew install saxon-b

ทำการแปลง OWASP ZAP XML Report เป็น JUnit XML Report

saxon -s:report.xml -xsl:zap2junit.xsl -o:junit.xml

Saxon เป็นโปรแกรมประมวลผล XSLT เพื่อแปลงไฟล์รายงาน OWASP ZAP (report.xml) ให้อยู่ในรูปแบบ JUnit (junit.xml) โดยมีรายละเอียดดังนี้:

  • saxon: เรียกใช้โปรแกรม Saxon XSLT processor
  • -s:report.xml: ระบุไฟล์ input ซึ่งในที่นี้คือไฟล์รายงาน OWASP ZAP ในรูปแบบ XML ชื่อ report.xml
  • -xsl:zap2junit.xsl: ระบุไฟล์ XSLT stylesheet ที่จะใช้ในการแปลง ซึ่งในที่นี้คือ zap2junit.xsl ซึ่งเป็น stylesheet ที่ออกแบบมาเพื่อแปลงรายงาน ZAP เป็นรูปแบบ JUnit
  • -o:junit.xml: ระบุไฟล์ output ซึ่งในที่นี้คือไฟล์ JUnit ชื่อ junit.xml ที่จะถูกสร้างขึ้นหลังจากการแปลง

ตัวอย่างของ junit.xml

<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite id="2" name="Medium (Medium)">
<testcase classname="" name="Buffer Overflow">
<failure
message="Description: Buffer overflow errors are characterized by the overwriting of memory spaces of the background web process, which should have never been modified intentionally or unintentionally. Overwriting values of the IP (Instruction Pointer), BP (Base Pointer) and other registers causes exceptions, segmentation faults, and other process errors to occur. Usually these errors end execution of the application in an unexpected way. Solution: Rewrite the background program using proper return length checking. This will require a recompile of the background executable. Reference: https://owasp.org/www-community/attacks/Buffer_overflow_attack " />
</testcase>
</testsuite>
<testsuite id="2" name="Medium (Medium)">
<testcase classname="" name="Source Code Disclosure - SQL">
<failure
message="Description: Application Source Code was disclosed by the web server. - SQL Solution: Ensure that application Source Code is not available with alternative extensions, and ensure that source code is not present within other files or data deployed to the web server, or served by the web server. Reference: https://www.wsj.com/articles/BL-CIOB-2999 " />
</testcase>
</testsuite>
<testsuite id="1" name="Low (High)">
<testcase classname="" name="A Server Error response code was returned by the server">
<failure
message="Description: A response code of 500 was returned by the server. This may indicate that the application is failing to handle unexpected input correctly. Raised by the 'Alert on HTTP Response Code Error' script Solution: Reference: " />
</testcase>
</testsuite>
<testsuite id="1" name="Low (Medium)">
<testcase classname="" name="Application Error Disclosure">
<failure
message="Description: This page contains an error/warning message that may disclose sensitive information like the location of the file that produced the unhandled exception. This information can be used to launch further attacks against the web application. The alert could be a false positive if the error message is found inside a documentation page. Solution: Review the source code of this page. Implement custom error pages. Consider implementing a mechanism to provide a unique error reference/identifier to the client (browser) while logging the details on the server side and not exposing them to the user. Reference: " />
</testcase>
</testsuite>
<testsuite id="1" name="Low (Medium)">
<testcase classname="" name="Insufficient Site Isolation Against Spectre Vulnerability">
<failure
message="Description: Cross-Origin-Resource-Policy header is an opt-in header designed to counter side-channels attacks like Spectre. Resource should be specifically set as shareable amongst different origins. Solution: Ensure that the application/web server sets the Cross-Origin-Resource-Policy header appropriately, and that it sets the Cross-Origin-Resource-Policy header to 'same-origin' for all web pages. 'same-site' is considered as less secured and should be avoided. If resources must be shared, set the header to 'cross-origin'. If possible, ensure that the end user uses a standards-compliant and modern web browser that supports the Cross-Origin-Resource-Policy header (https://caniuse.com/mdn-http_headers_cross-origin-resource-policy). Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy " />
</testcase>
</testsuite>
<testsuite id="1" name="Low (High)">
<testcase classname="" name="Unexpected Content-Type was returned">
<failure
message="Description: A Content-Type of text/html was returned by the server. This is not one of the types expected to be returned by an API. Raised by the 'Alert on Unexpected Content Types' script Solution: Reference: " />
</testcase>
</testsuite>
<testsuite id="0" name="Informational (High)">
<testcase classname="" name="A Client Error response code was returned by the server">
<failure
message="Description: A response code of 404 was returned by the server. This may indicate that the application is failing to handle unexpected input correctly. Raised by the 'Alert on HTTP Response Code Error' script Solution: Reference: " />
</testcase>
</testsuite>
<testsuite id="0" name="Informational (Low)">
<testcase classname="" name="Authentication Request Identified">
<failure
message="Description: The given request has been identified as an authentication request. The 'Other Info' field contains a set of key=value lines which identify any relevant fields. If the request is in a context which has an Authentication Method set to &#34;Auto-Detect&#34; then this rule will change the authentication to match the request identified. Solution: This is an informational alert rather than a vulnerability and so there is nothing to fix. Reference: https://www.zaproxy.org/docs/desktop/addons/authentication-helper/auth-req-id/ " />
</testcase>
</testsuite>
<testsuite id="0" name="Informational (Medium)">
<testcase classname="" name="Non-Storable Content">
<failure
message="Description: The response contents are not storable by caching components such as proxy servers. If the response does not contain sensitive, personal or user-specific information, it may benefit from being stored and cached, to improve performance. Solution: The content may be marked as storable by ensuring that the following conditions are satisfied: The request method must be understood by the cache and defined as being cacheable (&#34;GET&#34;, &#34;HEAD&#34;, and &#34;POST&#34; are currently defined as cacheable) The response status code must be understood by the cache (one of the 1XX, 2XX, 3XX, 4XX, or 5XX response classes are generally understood) The &#34;no-store&#34; cache directive must not appear in the request or response header fields For caching by &#34;shared&#34; caches such as &#34;proxy&#34; caches, the &#34;private&#34; response directive must not appear in the response For caching by &#34;shared&#34; caches such as &#34;proxy&#34; caches, the &#34;Authorization&#34; header field must not appear in the request, unless the response explicitly allows it (using one of the &#34;must-revalidate&#34;, &#34;public&#34;, or &#34;s-maxage&#34; Cache-Control response directives) In addition to the conditions above, at least one of the following conditions must also be satisfied by the response: It must contain an &#34;Expires&#34; header field It must contain a &#34;max-age&#34; response directive For &#34;shared&#34; caches such as &#34;proxy&#34; caches, it must contain a &#34;s-maxage&#34; response directive It must contain a &#34;Cache Control Extension&#34; that allows it to be cached It must have a status code that is defined as cacheable by default (200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501). Reference: https://datatracker.ietf.org/doc/html/rfc7234 https://datatracker.ietf.org/doc/html/rfc7231 https://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html " />
</testcase>
</testsuite>
</testsuites>

--

--

Nont Banditwong

Cloud Engineering Specialist, Software Developer, System Engineer, Photographer and Learner