この記事は7907文字約20分で読めます

Table of Contents

このブログを支える技術、第二段です。

みなさま、お疲れさまでございます。

本年も始まってしまい、すっかり仕事モードかと思いますが、前回に引き続きこのブログを支えてる技術をご紹介したいと思います。

今回はセキュリティ編、ということでSnykOWASP ZAPを使って お金をかけない脆弱性診断 を個人開発でも実現できる方法で検討していきたいと思います。

Snyk

Snyk(スニーク)というサービスを皆さまご存じでしょうか?

長年、特にDependabotがGitHubで使えるようになる前からライブラリのセキュリティ対策に興味のあった人ならご存じかもしれません。

公式の日本語Webサイトでは次のようにサービスが紹介されております。

Snyk(スニーク)は、安全な開発を迅速に行うことを支援しています。コードやオープンソースとその依存関係、コンテナやIaC(Infrastructure as Code)における脆弱性を見つけるだけでなく、優先順位をつけて自動的に修正します。

Gitや統合開発環境(IDE)、CI/CDパイプラインに直接組み込むことができるので、デベロッパーが簡単に使うことができます。

かくいう私もSnyk自体は2019年くらいから使ってました。長らくPythonで作ったプロジェクトの依存関係(Dependencies)更新に使ってました。ただこちら、情報のアップデートができてませんでした。

ずっとライブラリの依存関係ファイル(Dependencies file)から更新検知および更新の適用をしてくれるサービスだと思ってましたが、 コード・コンテナ・IaCなんかにも使える んですね。知らなかった。

久しぶりにSnykを触って、いくつか 個人的にいいな〜 と思った機能があったのでそちらのご紹介とGitHub Actionsへの載せ方を少し検討してみたいと思います。

Snyk CLIを使った依存関係の脆弱性チェック

まず、Snyk CLIを使った脆弱性チェックを実施します。

Snyk CLIを使うにはSnyk CLIをインストールする必要があるのですが、こちらはnpm, yarn経由でかんたんにインストールできます。

yarn global add snyk

# または

npm install -g snyk

ローカルでSnykを実行する場合は、 認証リンクによるログイン が必要となります。

Snyk auth

がしかし、今回はCI経由から実行するため認証リンクを踏んでいく手順は使えないため、あらかじめ SNYK_TOKEN を環境変数に設定しておきます。

SNYK_TOKENはアカウント設定から取得できます。GitHub ActionsのSecretsなどに仕込んでおきましょう。

依存関係の脆弱性チェックをする際はプロジェクトルートで下記のコマンドを実行するだけです。

snyk test .

すると、次のような依存関係のパッケージの脆弱性がリストされました。 (報告された脆弱性...。ちゃんと対応します...)

Testing ....

Tested 1657 dependencies for known issues, found 6 issues, 16 vulnerable paths.


Issues to fix by upgrading:

  Upgrade gatsby-transformer-remark@5.23.1 to gatsby-transformer-remark@6.0.0 to fix
  ✗ Regular Expression Denial of Service (ReDoS) [Medium Severity][https://security.snyk.io/vuln/SNYK-JS-SANITIZEHTML-2957526] in sanitize-html@2.3.2
    introduced by gatsby-transformer-remark@5.23.1 > sanitize-html@2.3.2


Issues with no direct upgrade or patch:
  ✗ Regular Expression Denial of Service (ReDoS) [High Severity][https://security.snyk.io/vuln/SNYK-JS-ANSIREGEX-1583908] in ansi-regex@2.1.1
    introduced by gatsby@4.25.1 > gatsby-cli@4.25.0 > pretty-error@2.1.2 > renderkid@2.0.7 > strip-ansi@3.0.1 > ansi-regex@2.1.1
  This issue was fixed in versions: 3.0.1, 4.1.1, 5.0.1, 6.0.1
  ✗ Denial of Service (DoS) [High Severity][https://security.snyk.io/vuln/SNYK-JS-DECODEURICOMPONENT-3149970] in decode-uri-component@0.2.1
    introduced by gatsby@4.25.1 > query-string@6.14.1 > decode-uri-component@0.2.1 and 1 other path(s)
  This issue was fixed in versions: 0.2.2
  ✗ Regular Expression Denial of Service (ReDoS) [Medium Severity][https://security.snyk.io/vuln/SNYK-JS-HTMLMINIFIER-3091181] in html-minifier@4.0.0
    introduced by html-minifier@4.0.0
  No upgrade or patch available
  ✗ Prototype Pollution [Medium Severity][https://security.snyk.io/vuln/SNYK-JS-JSON5-3182856] in json5@1.0.1
    introduced by babel-loader@8.3.0 > loader-utils@2.0.4 > json5@2.2.1 and 9 other path(s)
  This issue was fixed in versions: 1.0.2, 2.2.2
  ✗ Command Injection [High Severity][https://security.snyk.io/vuln/SNYK-JS-LODASHTEMPLATE-1088054] in lodash.template@4.5.0
    introduced by gatsby-plugin-offline@5.23.1 > workbox-build@4.3.1 > lodash.template@4.5.0
  No upgrade or patch available



Organization:      tubone24
Package manager:   yarn
Target file:       yarn.lock
Project name:      blog
Open source:       no
Project path:      .
Licenses:          enabled

ちなみに、指摘されている脆弱性ですが、レポジトリ設定で別で回しているDependabotでも同様のアラートが出ております。

Dependabotとの検知力の差はわかってませんがおそらく情報提供元はCVEと思うのでほぼ大差はないかと思います。

依存関係の脆弱性チェックだけであればSnyk、Depandabotどちらを使っても良さそうに思いました。

ただ、SnykのほうがDependabotに比べリンク先のリファレンスがわかりやすい気がしました。

ref snyk

Depandabotも書いてある情報は大差ないのですが、なんとなくSnykのほうが見やすい..。

ref dependabot

ちなみに、依存関係の脆弱性はSnyk CLIを使わずともGitHubレポジトリと連携するだけでSnyk上で勝手に依存関係を使っているパッケージ管理システム(npmとか)を追いかけて調べてくれます。

なので、SnykとGitHubレポジトリとの連携を済ませていれば、必ずしもSnyk CLIで実行する必要はないとは思います。

snyk dashboard

ただ、今回はCI/CDのトリガーと連携したいなと思い、(PRのOpenに合わせて起動させる、など)Snyk CLI経由で依存関係の脆弱性をチェックすることにしたというわけです。

Snyk CLIを使ったコードの静的チェック

Snyk CLIには静的コード解析で脆弱なコードをスキャンしてくれる機能もあります。

使い方は超カンタンで、プロジェクトルート、もしくはソースコードの含まれるディレクトリで、

snyk code test .

を実行するだけ。すると、

Testing . ...

 ✗ [Medium] Open Redirect 
   Path: src/templates/index.tsx, line 133 
   Info: Unsanitized input from the document location flows into url, where it is used as an URL to redirect the user. This may result in an Open Redirect vulnerability.


✔ Test completed

Organization:      tubone24
Test type:         Static code analysis
Project path:      .

Summary:

  1 Code issues found
  1 [Medium] 

という感じでOpen redirectの問題を検知してくれました。

せっかくなので、ちょっと直していきましょう。エラーになったコードは次のようなTSXでした。

     <div>
        <main>
            なんらかのページ
        </main>
        <div className="" />
      </div>
      <ShareBox
        url={String(location.href)}
        hasCommentBox={false}
      />

ここで問題になっているShareBoxコンポーネントですが、表示されているページのURLをlocation.hrefなどで、渡すことでTwitterやFacebookのShare Linkを作成する、というものです。

sharebox

このブログの正規なページにしか表示されないコンポーネントなので特に不正なリダイレクトURLを埋め込まれるような使い方は、正直あまり想定されてないコンポーネントですが、ブログ内で不正はURLへ遷移し、何かバグがあって不正なページでたまたまShareboxを開かれてしまうと、

任意のShare Linkが作れてしまうかもしれません。(繰り返しになりますが、不正なURL404 Not Foundページに遷移するので通常ShareBoxは出ません...。)

こちらOpen redirectのリファレンスを参考に、ドメインを固定することで攻撃を回避できそうです。

Snykの素晴らしいところはこのようのSnykのリファレンスがしっかりしているので 「なぜ、このコードが危ないのか?」「どう直せばいいのか?」 というところまで言及している点だと思います。すばらしい..。

      <ShareBox
        url={config.siteUrl + String(location.pathname)}
        hasCommentBox={false}
      />

これで、再度テストを実行し問題が解消したことが確認できました!

Testing . ...


✔ Test completed

Organization:      tubone24
Test type:         Static code analysis
Project path:      .

Summary:

✔ Awesome! No issues were found.

GitHub Actionsに組み込んでみる

さて、それではSynk CLIの実行結果を毎回のPR Openごとに取得し、PRのコメントとして通知する仕組みを検討します。

Synk CLIには結果をJSONで吐き出してParseする方法もありますが、今回は標準出力をそのままGitHubのPRコメントにする方法で行きたいと思います。

name: Snyk
on:
  workflow_dispatch:
  pull_request:
    branches:
      - master
    paths:
      - functions/**
      - src/**
      - test/**
      - terraform/**
      - '!src/content/**'
env:
  cache-version: v1

jobs:
  snyk:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout source code
        uses: actions/checkout@v3
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 16.x
      - name: snyk install
        env:
        run: yarn global add snyk
      - name: run snyk
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        run: |
          snyk test . > snyk.txt || :
          snyk code test . > snyk_code.txt  || :
      - name: summarize
        run: |
          cat snyk.txt
          cat snyk_code.txt
          touch summarize.txt
          echo '# Snyk vulnerability report' >> summarize.txt
          echo '## OSS packages' >> summarize.txt
          echo '' >> summarize.txt
          echo '' >> summarize.txt
          echo '<details>' >> summarize.txt
          cat snyk.txt | sed -z 's/\n/\\n/g' >> summarize.txt
          echo '</details>' >> summarize.txt
          echo '' >> summarize.txt
          echo '' >> summarize.txt
          echo '## Application' >> summarize.txt
          echo '' >> summarize.txt
          echo '' >> summarize.txt
          echo '<details>' >> summarize.txt
          cat snyk_code.txt | sed -z 's/\n/\\n/g' >> summarize.txt
          echo '</details>' >> summarize.txt
          echo '' >> summarize.txt
          sed -i -z 's/\n/\\n/g' summarize.txt
          sed -i 's/Testing \.\.\.\.//g' summarize.txt
          sed -i 's/Testing \. \.\.\.//g' summarize.txt
      - name: Post snyk Report Comment
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          URL: ${{ github.event.pull_request.comments_url }}
        run: |
          curl -X POST \
               -H "Authorization: token ${GITHUB_TOKEN}" \
               -d "{\"body\": \"$(cat summarize.txt)\"}" \
               ${URL}

力技ですが上のようにSnykコマンドの標準出力をいったんtxtで吐き出して、必要なフォーマット・エスケープ処理を行なったのち、結果をon.pull_requestイベントの際に取れる、github.event.pull_request.comments_urlにPOSTします。

preview

こんな感じで、GitHubのPRにSnyk結果が投稿されるようになりました!

OWASP ZAP

Snykを使って脆弱性になりえるコードやパッケージの排除はできましたが、静的コード解析だけではなく能動的な脆弱性を突く攻撃を実施し、自分たちのアプリケーションそのものへのアクティブな脆弱性診断もぜひ実施していきたいです。

SynkはSAST(Static Application Security Testing)、OWASP ZAPはDAST(Dynamic Application Security Testing)の使い分けを想定してます。

ただ、本格的な脆弱性診断は実施コストが大きいこともあり、かんたんにできるものではないのもまた事実です。

特に個人開発では実施ハードルはかなり高いです。

また、この手の診断は日々コードを改修していく際に脆弱性が混入することが多いので定期的にやってこそ価値があるものですが、コストの兼ね合い、内製化しようとしても実施者のスキル等で定期的に実施する仕組みを作っていくのも難しいものです。できれば自動で実施が定期的にできるといいですね。

そんなツラミを解決してくれる手段の1つが今回ご紹介するOWASP ZAPです。

OWASP ZAP(オワスプザップ)とは、OWASPが提供するOSS版のWebアプリケーション脆弱性診断ツールです。

Java製のクライアントソフトの他、CLI、Docker Imageなんかも公開されています。

注意

ここで気をつけなければいけないのが、OWASP ZapのActive Scanは設定によってはめちゃくちゃシステムへの攻撃を積極的に実施します。

ある意味クラッカーさんがやる攻撃手法をやってみて、脆弱性につながる応答が返ってこないかをみるのが目的のため当たり前の動作ですが、 決して自分の管理しているシステム以外には実施しないでください!!!

公式でも下記のように言及があります。

Active scanning is an attack on those targets.

You should NOT use it on web applications that you do not own.

また、自分が管理しているシステムでも、利用しているクラウドサービスなどでは追加の申請が必要なケースもあります。 十分に調べてから実施をおすすめします。

加えて、専門機関による脆弱性診断は合わせて実施されることがやはりおすすめです。OWASP ZAPでの検知力では限界があるため、公式でも手動のペネトレーションテストと併用することをおすすめしてます。お金に余裕のあるプロジェクトはちゃんと依頼しましょう。

It should be noted that active scanning can only find certain types of vulnerabilities.

Logical vulnerabilities, such as broken access control, will not be found by any active or automated vulnerability scanning.

Manual penetration testing should always be performed in addition to active scanning to find all types of vulnerabilities.

(本ブログでは、OWASP ZAPの実施・結果に関しての一切の責任は追いませんこと、ご了承ください。)

今回はそういった紛らわしいことをナシでいけるように、手軽に無料でOWASP ZAP Active ScanをGitHub Actions内に構築したクローズドなコンテナに対して実行 することで、安全に脆弱性診断を実施し、診断レポートも出力することを目的にします。その点だけあしからず..。

Owasp ZAPの起動方法(Web GUI)

Owasp ZAP公式からDocker Imageが配布されているため、提供されているDocker Imageを使えばかんたんにOWASP ZAPの起動ができます。便利ですね。

WebGUIの他、CIで使いやすいようにCLIも用意されてますが、まずはScan内容が見やすいGUIでローカル実行していきます。

ローカル環境にてOwasp ZAP Web GUIを起動するなら下記のようなDocker Composeを作ってあげることで、かんたんに立ち上げ可能です。

version: '3'

services:
  web:
    container_name: web-target
    build:
      context: ../
      dockerfile: owasp/Dockerfile
    command:
      - "npx"
      - "serve"
      - "-s"
      - "-l"
      - "9000"
      - "public"
    ports:
      - "9000:9000" # yarn serve
      - "8000:8000" # yarn dev
    networks:
      - myNW
    tty: true

  owasp:
    container_name: owasp-web-ui
    image: owasp/zap2docker-stable
    command: bash -c "zap.sh -cmd -addonuninstall hud && zap-webswing.sh"
    volumes:
      - ./zap:/zap/wrk/
    ports:
      - "18081:8080"
      - "18090:8090"
    depends_on:
      - web
    networks:
      - myNW
      - default
    tty: true

networks:
  myNW:
    internal: true

今回は簡易的に、下記2コンテナで実施します。

  • web-target: 攻撃対象、つまり自身のアプリケーションが動くコンテナです。
    • 例では簡易的にserveを使ってビルド済みのブログサイトファイルをホスティングしているだけの環境です。みなさまのテストしたいアプリケーションのDocker Imageを起動させてあげる形で読み替えてください。
      • このブログではGatsby.jsをビルドしたファイルを、Netlifyにホスティングしているだけなので、そもそもDocker container上で動くWebアプリケーションではありません。なので、serveのインフラ設定もいい加減で、インフラ周りの診断結果警告が出てくる箇所は実際のNetlify環境とは異なります。
      • みなさまの環境でもAWS WAFなどが実際にリクエストをブロックしたりするケースも多いので、診断レポート結果の精査 は必要です。
    • 今回のポイントとしてdocker container networkをInternalにしてOWASP ZAP以外はテスト対象のアプリケーションにアクセスできないようにしております。
      • 万が一問題があるアプリケーションをテストした場合に、外部に通信経路があるとそこから侵入されてしまうこともあるからです。石橋を叩いて渡ろう。
  • owasp-web-ui: OWASP ZAPが動くコンテナです
    • 私が起動している別のアプリケーションの都合で便宜的に8080ポートを18081変換して設定してます。
    • http://localhost:18080/zap にアクセスすることでOWASP ZAPのWeb GUIが起動します。
    • 18090(8090)ポートはProxy設定を行なうことで、OWASP ZAP経由でdocker container internal networkに作ったweb-targetにアクセスできますよ、というものです。今回は割愛します。

docker compose upしたら早速、OWASP ZAPのWEB GUIにアクセスしてみましょう。http://localhost:18080/zapからアクセスできます。

docker compose up

owasp splash

上のようなSplashのあと、操作画面に遷移します。

とりあえず一通りの脆弱性チェックを実施してみたいので、Automated Scanを選びweb-targetのコンテナhttp://web:9000をターゲットに設定します。

また、Scan modeStandard(Active Scan含む) にします。(Protectedだとpassive scanのみ実施します。)

active scan

Scan前に対象のアプリケーションのエンドポイントをかき集めるクロール(Spider)が実行されますが、Spiderも通常のSpiderとAjax Spiderの2種類が存在します。このブログはSPAの動きも多いのでAjax Spiderを選択してます。

owaspzap

Automated ScanのAjax Spiderは、起点URLとそのサブツリーのみを対象にするらしく、Spiderの過程で見つかった外部リンクはOut of Scoreになるようです。そのほうが不正な攻撃で迷惑がかからないので都合がいいです。

ajax spider

Spiderが終わるとそのままPassive Scan、Active Scanに入ります。検知されたURLの量にもよりますが30分から1時間くらいは平気でかかります。

scan

SQL InjectionXSSなど色々な攻撃をしてますね...。

attack

すべてのScanが完了すると検知した脆弱性が表示されます。

結構誤検知(False positive)がありました。技術ブログなので、Internal Server Errorなどの文言がブログ中に散見され、Information Disclosure - Debug Error Messages系の誤検知が増えてしまったものと思われます。

false positive   といった具合に出てきた診断レポートについて精査する必要はありますが、ひとまずScanはできたようです。

本当は、False positiveになったテストはAlert filterに設定して次回以降のテストに引っかからないようにする必要があります。

GUIではかんたんに設定できるのですが、同じ設定をCLIに引き継ぐ方法についてはこちらのIssueでやり取りされているものの、まだ自分の方でも上手くいってません。

この点は脆弱性診断でも検知率を向上させる重要な機能なので完成次第ブログを更新しようと思います。

今回はAlert filterの再設定は割愛することにして次に進みます。

GitHub Actionsに組み込む

さて、Web GUIでうまくいったScanについてそのままCI(GitHub Actions)にも組み込んでいきます。

とはいってもさきほど起動したOWASP ZAPのDocker containerで専用のPythonコードを実行してあげるだけです。

次のようなGitHub Actionsを作ることで実現可能です。

name: OWASP ZAP Actions
on:
  workflow_dispatch:
    inputs:
      spider-min:
        default: 0
        type: string
env:
  cache-version: v1
jobs:
  website-scan:
    runs-on: ubuntu-latest
    name: DAST (Dynamic Application Security Testing)
    steps:
      - name: Checkout source code
        uses: actions/checkout@v3
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: 16.x
      - name: yarn install
        run: yarn install --frozen-lockfile
      - name: yarn build
        run: yarn build
      - name: Action Full Scan
        run: |
          chmod 777 owasp/zap
          docker-compose -f owasp/docker-compose-ci.yml up -d
          docker-compose -f owasp/docker-compose-ci.yml exec -T owasp zap-full-scan.py -t http://web:9000 -r report.html -a -d -m ${{ inputs.spider-min }} -j -I -z "-config alert.maxInstances=0 -config view.locale=ja_JP"
      - name: Deploy Report
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./owasp/zap/
          destination_dir: owasp
          keep_files: true
          exclude_assets: '*.cer,*.key'

docker composeコマンドで docker-compose-ci.yml とさきほどとは別のcomposeファイルを呼び出してますが、こちらは単にttyをoffにしているだけです。

ttyがonだとActions内でコンテナが上手く立ち上がらなかったです。

実際のテスト実行はzap-full-scan.pyを使います。

docker-compose -f owasp/docker-compose-ci.yml up -d
docker-compose -f owasp/docker-compose-ci.yml exec -T owasp zap-full-scan.py -t http://web:9000 -r report.html -a -d -m ${{ inputs.spider-min }} -j -I -z "-config alert.maxInstances=0 -config view.locale=ja_JP"

という具合で、まず、検査対象のweb-targetコンテナと、owasp zapコンテナを立ち上げ、 その後、docker execコマンドで zap-full-scan.py を実行してあげることでFull Scanが実行されます。

zap-full-scan.pyのoptionsは公式のZAP - Full Scanに記載がありますのでこちらでは詳細割愛します。

-r オプションでレポートの出力ができます。こちらGitHub Actionsで出力したものを後々確認したいので、peaceiris/actions-gh-pages@v3を使いGitHub Pagesにアップロードすることにしました。

また、ZAP実行はon.workflow_dispatchのみにしてます。理由はOWASP ZAPの実行はかなり時間がかかるため、毎度のPRで実行するととてもじゃないですが、耐えられる待ち時間でないため、on.pull_requestなどにはできないからです。

on.workflow_dispatchで定期的に手動実行という形でいったん進めてますが、ActionsのUsageに余裕のある人はon.scheduleでの定期実行なんかもで良さそうです。

結果の出力

すべてのテストが完了すると、このようにHTML形式で診断レポートを出力します。GitHub Pagesにあげているので、URLにアクセスすれば確認できます。

report

結構きれいなレポートですね。

Information Disclosure - Debug Error MessagesPrivate IP Disclosureはブログ記事やロゴのSVGに含まれる文字列を引っ張って検知してしまった誤検知でした。

このあたりをレポートに出さなくする方法はさきほどお話したAlert filterで解決できますが、CLIへの渡し方についてはもうちょっと調査が必要な状況です。

また、レベル高Cloud Metadata Potentially Exposedが出てしまってますが、こちらはテスト用のDocker containerの設定によるものでこのブログ自身にはセキュリティイシューないことを確認済みです。

Content Security Policyなんかのエラーは利用者の安全を考えても今後直していきたいですね..。

結論

このように、SnykOWASP ZAPを使えば、それなりにしっかりした脆弱性チェックができることがお分かりいただけたのではないでしょうか。

今年は年始めから色々セキュリティに関するインシデントが乱発してますが皆さんのアプリケーションもしっかり対策して「今日も一日ご安全に!」いきましょう!

goanzenni

tubone24にラーメンを食べさせよう!

ぽちっとな↓

Buy me a ramen

 Related Posts

hatena bookmark