好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

Pythonコードの安全を保つSAST(静的解析)ツール ~Bandit, Pyt~

Python その2 Advent Calendar 2018 16日目の投稿が空いていたので、めっちゃ日が過ぎてますが飛び込み投稿。

今回は、昨今よく聞くようになった「DevSecOps」(DevOps + Security) 活動で重要になってくる、「セキュリティテストも自動で回す」を実現するためのツールを紹介します。
DevSecOpsについてはこのあたりを参照。2018年のトレンドらしいです。

2018年のトレンドは、DevOpsにセキュリティを融合した「DevSecOps」 (1/2) - ITmedia エンタープライズ

自動セキュリティテストには SAST, DAST, IAST と呼ばれるものがあります。

  • SAST: Static Application Security Testing(静的セキュリティ検査)
    • ソースコード自体を解析・検査して脆弱性を見つけ出すもの
    • 動くコードになる前の段階から使用できる
  • DAST: Dynamic Application Security Testing(動的セキュリティ検査)
  • IAST: Interactive Application Security Testing
    • SASTとDASTの双方のメリットを兼ね合わせた手法
    • まずDASTで検査を行い、疑わしい部分をSASTで解析する

今回はSASTに焦点を当てて、Pythonで使えるSASTツール2点を紹介します。

Bandit

Banditとは

Bandit の意味は、山賊。なぜこのネーミングかは調べてません(ノ≧ڡ≦)
公式のREADMEより

Banditは、Pythonコードで共通のセキュリティ問題を見つけるために設計されたツールです。 これを行うために、Banditは各ファイルを処理し、そこからASTを作成し、ASTノードに対して適切なプラグインを実行します。 Banditはすべてのファイルのスキャンを完了すると、レポートを生成します。 BanditはもともとOpenStack Security Project内で開発され、その後PyCQAに改造されました。

github.com

Bandit 公式ドキュメント:Security/Projects/Bandit

以下、Bandit ソースリポジトリ(Github) のREADMEに使い方に沿って実施してみます。

installと実行

install: pipでinstallできちゃいます。簡単。

$ pip install bandit

実行: 基本的に bandit {テスト対象のパス} ですが、ほとんどのプロジェクトがディレクトリ構造を思っていると思うので、再帰optionの -r をつけて実行します。

$ bandit -r {path/to/your/code}

これだけ。簡単。

主要なオプション

  • html出力
    • -f html
  • 出力ファイルの指定
    • -o {output file}
  • テスト対象の指定・除外と、実施テストの指定・除外
    • .bandit ファイルをテスト対象pathに配置
    • targets: テスト対象を指定
    • exclude: テスト対象からの除外を指定
    • skips: skipするテストの指定
    • tests: 実施するテストの指定

例: (.bandit)

[bandit]
targets: /app
exclude: /test
skips: B102
tests: B101, B301

※テストのIDは、Bandit repositoryのREADMEに書いてあります

  • 除外設定
    • Banditでは警告が出るが、その行がレビューされ、偽陽性・許容可能である場合に #nosec というコメントを対象の行につけることで、banditのレポート対象外になります。

例:

self.process = subprocess.Popen('/bin/echo', shell=True)  # nosec

実行結果と分析

とあるサービスのfrontコードを解析した結果のサマリ

Code scanned:
    Total lines of code: 2754
    Total lines skipped (#nosec): 0

Run metrics:
    Total issues (by severity):
        Undefined: 0.0
        Low: 0.0
        Medium: 1.0
        High: 0.0
    Total issues (by confidence):
        Undefined: 0.0
        Low: 0.0
        Medium: 0.0
        High: 1.0
Files skipped (0):

指摘されたコード部分(severity: Medium, confidence: High)を詳しく見てみます。

yaml_load: Use of unsafe yaml load. Allows instantiation of arbitrary objects. Consider yaml.safe_load().
Test ID: B506
Severity: MEDIUM
Confidence: HIGH

yaml.load() ではなく yaml.safe_load() を使いなさいとのこと。
yaml.load()脆弱性に関してはこちら参照。

他、こんな指摘の出たプロジェクトも。(severity: High, confidence: High)

The pyCrypto library and its module RSA are no longer actively maintained and have been deprecated. Consider using pyca/cryptography library.
Test ID: B413
Severity: HIGH
Confidence: HIGH

pyCrypto.RSApyCrypto.PKCS1_OAEP など、 pyCryptモジュール自体がもはや活発にメンテナンスされていないので、代わりに pyca/cryptography を使いなさいとのこと。 RSAのサンプルコードといえば pyCrypto というくらい出回っているので、こういうツールがないとなかなか気づきにくそう。

以下、セキュリティ勉強会のために作った脆弱性のあることがわかっているWebApplicationのソースを解析した結果です。terminal上では色付きで結果を表示してくれて十分見やすいです。

f:id:kusuwada:20181223021055p:plain f:id:kusuwada:20181223021102p:plain

ちなみに、-f html オプションを付けて実行すると、下記のような出力が得られます。

f:id:kusuwada:20181223021107p:plain

Pyt

Pytとは

公式のREADMEより

理論的基礎(制御フローグラフ、不動点解析、データフロー解析)に基づくPython Webアプリケーションの静的解析

特徴:

などの検出

github.com

PytはBanditとは異なり、Webアプリケーションに特化しています。このため、一般的なWebアプリケーション機能が有する routing 機能などがあるシステムを前提にしています。
明示的に対応しているWebFrameworkのは、Flask, Djangoですが、その他のFWも -a オプションと設定ファイルの記述により利用可能です。

installと実行

insatll: こちらもpipコマンドでインストールできます。

$ pip install python-taint

setting: Banditよりちょっと複雑。

  • まずは使用しているWebFrameworkを選択します。 default は Flaskです。WebFWは -a optionで選択します。詳細はこちら

    • flask: none
    • django: -a D ※判断基準はリンク先参照
    • Every: -a E ※判断基準はリンク先参照
    • Pylons: -a P ※判断基準はリンク先参照

    今回はFlaskなのでオプション無しで実行します。

  • テスト対象の設定ファイルをカスタマイズ・設定します

    • defaultではこちらが使われます
    • 対象のmethodの取捨選択や、チェック内容をカスタマイズできます
  • 検出したい関数をカスタマイズする

上記のように、ターゲット対象・対象外のdirectoryや除外設定はbanditと同じく設定可能です。

実行: こちらもオプション無しだとシンプルで、 pyt {テスト対象のパス} です。

$ pyt (-a ADAPTOR) {path/to/your/code}

-a オプションは使用しているWebFWによっては必須なので注意です。

実行結果と分析

とあるサービスのfrontコードを解析した結果

$ pyt -r ./app/front
No vulnerabilities found.

何も検出されませんでした。
なかなか指摘が出てくるプロジェクトがなかったため、先程も使用したSQL Injection の脆弱性があるとわかっているprojectを解析。

$ pyt -r ./web_1/sqli_1/flask
2 vulnerabilities found:
Vulnerability 1:
File: ./hello.py
 > User input at line 19, source "form[":
     name = request.form['Name']
Reassigned in:
    File: ./hello.py
     > Line 23: ~call_3 = ret_'SELECT * FROM users WHERE name='{name}' AND password='{password}''.format(name=name, password=password)
    File: ./hello.py
     > Line 23: query = ~call_3
File: ./hello.py
 > reaches line 25, sink "execute(":
    ~call_4 = ret_cursor.execute(query)
This vulnerability is unknown due to:  Label: ~call_3 = ret_'SELECT * FROM users WHERE name='{name}' AND password='{password}''.format(name=name, password=password)

Vulnerability 2:
File: ./hello.py
 > User input at line 20, source "form[":
     password = request.form['Password']
Reassigned in:
    File: ./hello.py
     > Line 23: ~call_3 = ret_'SELECT * FROM users WHERE name='{name}' AND password='{password}''.format(name=name, password=password)
    File: ./hello.py
     > Line 23: query = ~call_3
File: ./hello.py
 > reaches line 25, sink "execute(":
    ~call_4 = ret_cursor.execute(query)
This vulnerability is unknown due to:  Label: ~call_3 = ret_'SELECT * FROM users WHERE name='{name}' AND password='{password}''.format(name=name, password=password)

2つ検出されました!ちょっと長くてわかり辛い。
1つ目と2つ目は namepassword かの違いなので、1つ目を上から読み解くと、

> User input at line 20, source "form[":
  password = request.form['Password']

ここでUserからの入力があるでしょ。

Reassigned in:
     > Line 23: ~call_3 = ret_'SELECT * FROM users WHERE name='{name}' AND password='{password}''.format(name=name, password=password)

ここで代入されてるでしょ。

> reaches line 25, sink "execute(":
 ~call_4 = ret_cursor.execute(query)

そしたらここまで(ユーザーの入力が)到達しちゃうでしょ。

This vulnerability is unknown due to:  Label: ~call_3 = ret_'SELECT * FROM users WHERE name='{name}' AND password='{password}''.format(name=name, password=password)

この脆弱性(のカテゴライズ)は、以下の箇所が原因で不明です。

こんな感じでしょうか。
確かに SQL Injection が起きる原因となっている箇所がリストアップされていますが、具体的な理由・修正案は出力されませんでした。

pytにはhtml形式のレポートはないようなので、json形式で出力してみます。(-j オプションを追加して実行するだけ)

{
    "generated_at": "2018-12-21T05:32:30Z",
    "vulnerabilities": [
        {
            "source": {
                "label": "name = request.form['Name']",
                "line_number": 19,
                "path": "./hello.py"
            },
            "source_trigger_word": "form[",
            "sink": {
                "label": "~call_4 = ret_cursor.execute(query)",
                "line_number": 25,
                "path": "./hello.py"
            },
            "sink_trigger_word": "execute(",
            "type": "UnknownVulnerability",
            "reassignment_nodes": [
                {
                    "label": "~call_3 = ret_'SELECT * FROM users WHERE name='{name}' AND password='{password}''.format(name=name, password=password)",
                    "line_number": 23,
                    "path": "./hello.py"
                },
                {
                    "label": "query = ~call_3",
                    "line_number": 23,
                    "path": "./hello.py"
                }
            ],
            "unknown_assignment": {
                "label": "~call_3 = ret_'SELECT * FROM users WHERE name='{name}' AND password='{password}''.format(name=name, password=password)",
                "line_number": 23,
                "path": "./hello.py"
            }
        },
        {
            "source": {
                "label": "password = request.form['Password']",
                "line_number": 20,
                "path": "./hello.py"
            },
            "source_trigger_word": "form[",
            "sink": {
                "label": "~call_4 = ret_cursor.execute(query)",
                "line_number": 25,
                "path": "./hello.py"
            },
            "sink_trigger_word": "execute(",
            "type": "UnknownVulnerability",
            "reassignment_nodes": [
                {
                    "label": "~call_3 = ret_'SELECT * FROM users WHERE name='{name}' AND password='{password}''.format(name=name, password=password)",
                    "line_number": 23,
                    "path": "./hello.py"
                },
                {
                    "label": "query = ~call_3",
                    "line_number": 23,
                    "path": "./hello.py"
                }
            ],
            "unknown_assignment": {
                "label": "~call_3 = ret_'SELECT * FROM users WHERE name='{name}' AND password='{password}''.format(name=name, password=password)",
                "line_number": 23,
                "path": "./hello.py"
            }
        }
    ]
}

人の目で見るぶんには、特に読みやすさは変わりませんでした…。実行結果をスクリプトで解析したりなんかするぶんには使いやすそうです。

もう一つやってみます。次は XSS脆弱性があるとわかっている下記のソースです。

$ pyt -r ./web_1/xss_1/flask
1 vulnerability found:
Vulnerability 1:
File: ./hello.py
 > User input at line 14, source "form[":
     name = request.form['Name']
File: ./hello.py
 > reaches line 17, sink "render_template(":
    ~call_2 = ret_render_template('xss_form.html', name=name, secret=secret)

無事検出されました。今回は 14行目でユーザーのインプットを受け付けたものが、17行目のrender部分に直接到達していることを指摘されているようです。

ついでに最後はパストラバーサル脆弱性があるサービス。

$ pyt -r ./web_1/directory_1/flask
1 vulnerability found:
Vulnerability 1:
File: ./hello.py
 > User input at line 16, source "request.args.get(":
     ~call_1 = ret_request.args.get('image')
Reassigned in:
    File: ./hello.py
     > Line 16: image = ~call_1
    File: ./hello.py
     > Line 19: ~call_4 = ret_os.path.join(~call_5, 'static', image)
File: ./hello.py
 > reaches line 19, sink "send_file(":
    ~call_3 = ret_send_file(~call_4)

似たような感じですね。今回はURLのPathを取得する部分が User input の可能性のある場所として指摘されています。

まとめ

今回はPythonのSASTツールである Bandit と Pyt を紹介しました。
Banditのほうが結果の出力が見やすく、形式も csv, custom, html, json, screen, txt, xml, yaml とめちゃめちゃ選択肢があるため、ぱっと始めるにはとっつきやすそうです。出力内容も、Pytでは出てこなかった SQL Injection など具体的な脆弱性の名前が指摘されていました。

一方、同じコードを解析したところ、Pyt で指摘のあった内容で Bandit でも拾えなかったものもありました (XSSディレクトリトラバーサル)。SASTツールは複数のツールを導入することが脆弱性の早期発見に役立ちます。PytはBanditと比較して出力結果を読み解くのに訓練がいりそうですが、Banditとは違ったロジックで検査を行っています。
今回の2つのツールはinstallから実行まで導入障壁が少ない上に実行時間もごく短いことから、併用するのがお勧めです。

SASTツールはCIと大変相性がよく、commitやpushごとにUTやフォーマットチェックなどと合わせて実施しやすいかと思います。みなさんも是非2018年のトレンドである「DevSecOps」に乗っかって、SASTツールを開発・CIに導入してみませんか?

参考リンク