DAST（OWASPZAP）のスキャンをパイプラインで実行してみた

# OWASP ZAPを用いたWebアプリケーションのDAST実践と自動化対応

## はじめに

Webアプリケーションのセキュリティを担保するうえで、外部からの攻撃を模擬して脆弱性を洗い出す「DAST（Dynamic Application Security Testing）」は重要な手段です。本記事では、OWASP ZAP を用いてアプリケーションに対してDASTを実施した取り組みについて、実際に行ったことや得られた知見、苦労した点などを紹介します。

## この記事でわかること

- OWASP ZAP を使ったDASTの概要と使い方  
- CodeBuildによる自動化の手法  
- スキャン対象の制御（スコープ）方法   
- 認証や誤検知への対応例  

## OWASP ZAPについて

OWASP ZAP（Zed Attack Proxy）は、オープンソースで提供されているWebアプリケーション脆弱性スキャナーです。プロキシとして動作し、Webアプリに対して自動的または手動で攻撃を行い、脆弱性を検出します。  
ZAPはGUIとCLIの両方で使用でき、レポート出力やCI/CDへの統合も容易です。

---

## やったこと

### 1. ローカルからAmplifyへのOWASP ZAPスキャン実行

- **内容**  
  ローカル環境から、Amplifyにデプロイされたアプリケーションに対してZAPを用いてDAST（Dynamic Application Security Testing）を行いました

- **手順**
    1. Docker版のOWASPZAPコマンドを実行する(Full-Scanを実施)
        ```
        docker run --rm \
        -v $(pwd):/zap/wrk:rw \
        -t ghcr.io/zaproxy/zaproxy:stable \
        zap-full-scan.py \
        -t ${ZAP_TARGET_URL} \
        -n ./owaspzap.context \
        -a \
        -d \
        -j \
        -I \
        -J zap_report.json\
        -r zap_report.html \
        -z "-config replacer.full_list\\(0\\).description=auth1 \
        -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\\).regex=false \
        -config replacer.full_list\\(0\\).replacement='Basic ${BASE64_AUTH_HEADER}' \
        -config view.mode=protect"
        ```
        - 設定オプションの説明
            - -nは、スキャンスコープが定義されているファイルへの相対パス
            - -aは、アルファパッシブスキャン（リリース版ではなく、Alpha版を含むスキャンルールで実行）の有効化
            - -dは、デバックモードでの実行を有効化
            - -jは、Ajaxスパイダーの実行を有効化
            - -Iは、ステータスコードをSUCCESS(exit status 0)で返す
            - -Jは、JSON形式でレポートの出力
            - -rは、HTML形式でレポートの出力
            - -zは、zapオプションの指定（今回はBasic認証突破のためにヘッダー情報の書き換えとスキャンモードを設定している）
    1. レポートの確認 
    ![スクリーンショット 2025-05-19 14.00.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/ed42c51e-6564-471e-9a47-d516c351936b.png)



- **困ったこと**  
    - Basic認証の突破が必要だったが、認証ヘッダーの設定に手間取りました
      - コンテキストファイルに認証情報も含めようとしたが、うまくいかなかった
      - -zオプションで指定する場合に、`-config replacer.full_list\\(0\\).replacement='Basic ${BASE64_AUTH_HEADER}'`の
          ```
          'Basic ${BASE64_AUTH_HEADER}'
          ```
      のように途中でスペースがある場合はシングルクォートで囲む必要があることになかなか気づけなかった
    - `view.mode=protect`で実行するためには、スコープの指定が必須なのでcontextファイルで指定する必要がありました
    ※`view.mode=protect`は特定の範囲でのみスキャンするモード

- **わかったこと** 
    - JSON形式のレポートから、RiskLevelがMediumやHighのリスクを抽出することができること
        - これによってパイプラインで実行して通知する際にMedium以上が出た場合にのみ警告のメールを通知することができました 
        ```
        JSONレポートの一部
        "riskcode": "1", //リスクレベル0,1,2,3の順で,info,low,medium,highの順
        "confidence": "3", //確信度(この脆弱性に対しての自信の度合い)上記と一緒
        "riskdesc": "Low (High)", //リスクレベル(確信度)
        ```
    - `view.mode`を`protect`に設定することで、ZAPのスキャン対象を明示的に制御できること
    - `view.mode`を`atack`にすると、リンクが貼ってあったりした場合他のサイトにも攻撃を仕掛けることになって危険なのでやめた方が良いこと

---

### 2. ローカルからAppSync APIへのZAPスキャン実行

- **内容**  
  ZAPを使ってGraphQL API（AppSync）に対してもスキャンを試みました

- **手順**
    1. Docker版のOWASPZAPコマンドを実行する(API-Scanを実施)
        ```
        docker run --rm -v $(pwd):/zap/wrk/:rw ghcr.io/zaproxy/zaproxy:stable zap-api-scan.py \
        -t ${AWS_APPSYNCAPI_GRAPHQL_ENDPOINT_URL} \
        -f graphql \
        -a \
        -d \
        -I \
        -r zap_graphql_report.html \
        --schema ./schema.graphql \
        -z "-config replacer.full_list\\(0\\).description=authHeader \
        -config replacer.full_list\\(0\\).enabled=true \
        -config replacer.full_list\\(0\\).matchtype=REQ_HEADER \
        -config replacer.full_list\\(0\\).matchregex=false \
        -config replacer.full_list\\(0\\).matchstring=X-Api-Key \
        -config replacer.full_list\\(0\\).replacement='${APPSYNC_API_KEY}'"
        ```
        - 設定オプションの説明
            - -fは、フォーマットの指定(openapi,soap,もしくはgraphqlのいずれか)
            - -aは、アルファパッシブスキャン（リリース版ではなく、Alpha版を含むスキャンルールで実行）の有効化
            - -dは、デバックモードでの実行を有効化
            - -Iは、ステータスコードをSUCCESS(exit status 0)で返す
            - -rは、HTML形式でレポートの出力
    1. レポートの確認![スクリーンショット 2025-05-19 14.20.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3971030/71f98a3e-8254-4a33-aa96-aa10b1c6839b.png)



- **困ったこと**  
  - API認証（トークン認証）をZAPから突破する方法の情報が少なく、対応に苦慮しました
      - CloudWatchでログを確認し、Headerに'x-api-key'があるかどうかを確認すると作業がしやすくなるのでおすすめです
  - GrapgQLのschemaを指定する方法を悩みました
      - 今回はAmplifyのバックエンドとしてGraphQLエンドポイントを構築していたので、Amplifyデプロイ時にschemaファイルが作成されS3に保存されるので保存されたschemaファイルを指定しました(Appsyncからスキーマファイルを取得して指定する方法もある)

- **わかったこと**  
    - GraphQLスキャンを行うためにはスキーマファイルの準備が必須  
    - Basic認証と同様にHeaderに追加することで対応できること
    - -zで複数行を記述する際には、不要なスペースを削除する必要があること
        ```
        // OK
        -z "-config replacer.full_list\\(0\\).description=authHeader \
        -config replacer.full_list\\(0\\).enabled=true \
        -config replacer.full_list\\(0\\).matchtype=REQ_HEADER \
        -config replacer.full_list\\(0\\).matchregex=false \
        -config replacer.full_list\\(0\\).matchstring=X-Api-Key \
        -config replacer.full_list\\(0\\).replacement='${APPSYNC_API_KEY}'"
        ```
        ```
        // NG
        -z "-config replacer.full_list\\(0\\).description=authHeader \
            -config replacer.full_list\\(0\\).enabled=true \
            -config replacer.full_list\\(0\\).matchtype=REQ_HEADER \
            -config replacer.full_list\\(0\\).matchregex=false \
            -config replacer.full_list\\(0\\).matchstring=X-Api-Key \
            -config replacer.full_list\\(0\\).replacement='${APPSYNC_API_KEY}'"
        ```
- 参考
    - Dockerコマンド
        - https://www.zaproxy.org/docs/docker/
    - 検知する内容のリスト
        - https://www.zaproxy.org/docs/alerts/
    - -zオプション
        - https://www.zaproxy.org/blog/2017-06-19-scanning-apis-with-zap/

---

### 3. CodeBuildでZAPを実行

- **内容**  
  パイプライン上（CodeBuild）にZAPの実行処理を組み込み、毎日自動で実行されるようにしました。

- **困ったこと**  
    - フロントでのZAPのスコープ設定に必要な`context`ファイルの作成が難しかったです
        `context`ファイルは、OWASPZAPのGUIで設定を作成しエクスポートし全く同じ内容をCode Buildのymlファイル内で作成することで、`context`ファイルの読み込みを実現しました
        ```
            <?xml version="1.0" encoding="UTF-8" standalone="no"?>
            <configuration>
                <context>
                    <name>既定コンテキスト</name>
                    <desc/>
                    <inscope>true</inscope>
                    <incregexes>https://TARGETURL.*</incregexes>
                    <tech>
                        <include>Db</include>
                        <include>Db.CouchDB</include>
                        <include>Db.Firebird</include>
                        <include>Db.HypersonicSQL</include>
                        <include>Db.IBM DB2</include>
                        <include>Db.MariaDB</include>
                        <include>Db.Microsoft Access</include>
                        <include>Db.Microsoft SQL Server</include>
                        <include>Db.MongoDB</include>
                        <include>Db.MySQL</include>
                        <include>Db.Oracle</include>
                        <include>Db.PostgreSQL</include>
                        <include>Db.SAP MaxDB</include>
                        <include>Db.SQLite</include>
                        <include>Db.Sybase</include>
                        <include>Language</include>
                        <include>Language.ASP</include>
                        <include>Language.C</include>
                        <include>Language.JSP/Servlet</include>
                        <include>Language.Java</include>
                        <include>Language.Java.Spring</include>
                        <include>Language.JavaScript</include>
                        <include>Language.PHP</include>
                        <include>Language.Python</include>
                        <include>Language.Ruby</include>
                        <include>Language.XML</include>
                        <include>OS</include>
                        <include>OS.Linux</include>
                        <include>OS.MacOS</include>
                        <include>OS.Windows</include>
                        <include>SCM</include>
                        <include>SCM.Git</include>
                        <include>SCM.SVN</include>
                        <include>WS</include>
                        <include>WS.Apache</include>
                        <include>WS.IIS</include>
                        <include>WS.Tomcat</include>
                    </tech>
                    <urlparser>
                        <class>org.zaproxy.zap.model.StandardParameterParser</class>
                        <config>{"kvps":"&amp;","kvs":"=","struct":[]}</config>
                    </urlparser>
                    <postparser>
                        <class>org.zaproxy.zap.model.StandardParameterParser</class>
                        <config>{"kvps":"&amp;","kvs":"=","struct":[]}</config>
                    </postparser>
                    <forceduser>-1</forceduser>
                    <session>
                        <type>0</type>
                    </session>
                </context>
            </configuration>
        ```
- **わかったこと**  
    - -Iオプションを設定しておかないと、WARNが一つでも出るとビルド処理が失敗してしまい後続処理が行われなくなる
    - CodeBuild上でもローカルと同様にZAPが動作し、セキュリティチェックを自動化できるため、継続的なスキャン体制を構築するベースとなりました

---

### 4. 脆弱性対応（フロント・バックエンド）

- **内容**  
  スキャンで検出されたフロント・バックエンドの脆弱性に対して、修正・除外の対応を実施しました

- **対応内容**
    1. CSPヘッダーの設定(リスクレベル Medium)
        - CSPヘッダーとは
            - 「どのリソースをどこから読み込んでよいか」 を明示的にブラウザに伝えるセキュリティ機構
        - 予想されるリスク
            1. XSS（クロスサイトスクリプティング）
                攻撃者が悪意あるJavaScriptを仕込む。
                そのスクリプトがユーザーのセッションやCookieを盗む。
                被害：なりすまし、アカウント乗っ取りなど
            1. マルウェア配布
                外部の不正なJSファイルや画像を読み込ませて感染を狙う
            1. クリックジャッキング
                frame-ancestors 指定がないと、他のサイトにiframeで読み込まれて、ユーザーの操作を誘導される可能性あり
        - 対応参考サイト
            - https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/CSP

    1. 使用するHttpメソッドの指定(リスクレベル Medium)
        - 全てのHttpメソッドを許可している場合「TRACE」メソッドをつかい、プロキシサーバーの特定をされてしまう
        - 予想されるリスク
            - プロキシの悪用・踏み台化
                - 攻撃者がそのプロキシを利用して、外部への攻撃を行う可能性があります。
                - オープンプロキシ（誰でも利用可能）にされていると、匿名性を確保した攻撃に使われます。
                - あなたのIPが攻撃元としてログに残り、責任を問われるリスクがあります
        - ※ Amplifyを使っている場合CloudFrontがプロキシサーバーの役割をになっていますが、AmplifyによってCloudFrontが隠されているので、設定を変更することができません
            - 対応方法がないためこの警告は無視する設定を行う

    1. Cross-Originヘッダーの設定(リスクレベル Low)
        - Cross-Originヘッダーとは
            - 現在のページと異なるオリジンからのリソース・APIアクセスを制御するセキュリティ機構
        - 予想されるリスク
            - Cross-Origin-Embedder-Policy
                - SharedArrayBufferが使えない・Spectre攻撃に対する防御が弱い
            - Cross-Origin-Resource-Policy
                - リソース盗用、他サイトからの不正読み込み
            - Cross-Origin-Opener-Policy
                - タブジャック（tab-nabbing）や window.opener 経由の操作リスク

    1. Permissions-Policyヘッダーの設定(リスクレベル Low)
        - Permissions-Policyヘッダーとは
            - Webブラウザに対して「特定の機能（API）をこのページで使わせるかどうか」を制御するためのセキュリティ機能
        - 予想されるリスク
            - iframeに読み込まれた場合でも、機能が有効になってしまう可能性があります。
            - 悪意のある第三者サイトが、自サイトのiframe内で機能を悪用する（クリックジャッキング、位置情報の不正取得など）可能性
        - 対応参考サイト
            - https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Permissions_Policy

    1. Serverヘッダーを隠すための設定(リスクレベル Low)
        - Serverヘッダーとは
            - Webサーバーのソフトウェア情報（例：Apache, Nginx, IISなど）をクライアントに通知するもの
        - 予想されるリスク
            1. 攻撃対象の特定につながる
                - 攻撃者はサーバー種別やバージョンを見て、既知の脆弱性があるかどうかを調べます
                - 例：Apache 2.4.49 はパス・トラバーサル脆弱性（CVE-2021-41773）があり、バージョン情報が漏れていれば狙い撃ちされる可能性があります

            1. OSやミドルウェア情報も推測されやすくなる
                - UbuntuなどのOS名があると、それに特化した攻撃（例：パッケージ構成、ファイルパス）を試されやすくなります

            1. スキャナやボットによるターゲティングを受けやすい
                - 自動スキャンツールが、Server ヘッダーを見て攻撃対象として認識するケースもあります
        - Httpメソッドと同じくCloudFrontでヘッダーの設定を変更することで対応するため、対応不可なので無視する設定をする

    1. Strict-Transport-Securityヘッダーの設定(リスクレベル Low)
        - Strict-Transport-Securityヘッダーとは
            - Webサイトへのアクセスを常に HTTPS に強制するためのセキュリティヘッダー
        - 予想されるリスク
            1. ダウングレード攻撃（HTTPS→HTTP）
                - 攻撃者がネットワーク上で HTTPS を HTTP に強制ダウングレード（中間者攻撃）させることで、通信内容を盗聴・改ざんできます

            1. 中間者攻撃（MITM）
                - 公共Wi-Fiなどで、攻撃者がユーザーとWebサイトの通信の間に割り込み、セッションを乗っ取ったり、ログイン情報を盗んだりする可能性があります

            1. ユーザーの初回アクセスがHTTPになる場合の脆弱性
                - HSTSは一度HTTPSでアクセスした後にしか有効にならないため、初回HTTPアクセス時に攻撃されると意味がなくなります
                - これを防ぐには preload に登録し、最初からHTTPSしか許可しないようにします
        - 対応参考サイト
            - https://developer.mozilla.org/ja/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security

    1. X-Content-Type-Optionsヘッダーの設定(リスクレベル Low)
        - X-Content-Type-Optionsとは
            - 「コンテンツのMIMEタイプを勝手に推測して処理しないでね」という指示を出すためのHTTPレスポンスヘッダー
        - 予想されるリスク
            1. クロスサイトスクリプティング（XSS）攻撃のリスク増加
                - 例えば、サーバーから本来はテキストファイルや画像として送るべきものが誤ってHTMLやJavaScriptとして解釈されてしまうと、攻撃者が悪意のあるスクリプトを実行させることが可能になります

            1. 不正なファイルの誤解釈
                - 本来ダウンロードファイルとして渡すべきファイルがブラウザで勝手に実行されてしまうなどの問題
        - 対応参考サイト
            - https://developer.mozilla.org/ja/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options
    
    1. 対応不可、不要な警告を無視する設定をする
        - 手順
            1. Dockerでスキャンする際に-gコマンドを含めることで、Configファイルの作成をする
            1. 作成されたファイルの中から、無視したいスキャン項目を選び、別ファイルで記述する
            1. 記述する上でWARNをIGNOREに変更する
            1. `context`ファイルと同じように、`-c ignore.conf`のように指定する 
    
- **困ったこと**  
  - 脆弱性として出たものに対して、脆弱性対応に時間を要した
  - 脆弱性対応することによって、スキャン対象のアプリで影響が出てしまい、調査対応に時間を要した
  - Ignoreの設定において、configファイルに記述してスキャンを実行すると、無視されるものとされないものが存在した
      - ActiveScanにおいてはレポート上からも除外されているが、PassiveScanにおいてはログ上ではIgnore設定されているが、レポート上では除外されなくなっていた
      - 参考(github issue)
          - https://github.com/zaproxy/zaproxy/issues/5212

- **わかったこと** 
    - 特に何も対応していないサイトは、意外とセキュリティ面で不安があるものだということがわかった 
    - 使っているフレームワークによっては対応できないものがあること
    - 誤検知と思われるものは`Ignore`設定で除外しつつ、本当に影響のあるリスクを見極めることが重要

---

## まとめ

OWASP ZAP を使ったDASTは、無料かつ高機能な手段として非常に有効です。  
ただし、ZAPの利用にはスコープ設定や認証周りの工夫が必要であり、運用にはある程度の知識が求められます。

自動化と通知設定を適切に整備することで、CIパイプラインへの統合も可能となり、開発サイクル内でのセキュリティ担保が実現できます。  
OWASP ZAPは脆弱性診断でデフォルトスタンダードのツールと理解しており、情報はたくさんあるものだと思っておりましたがパイプラインで実行するための情報は意外と少なく苦労しました。
今回の記事が皆さんの一助になると嬉しいです