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に改造されました。
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: テスト対象からの除外を指定
/test
ディレクトリなど
- skips: skipするテストの指定
- tests: 実施するテストの指定
例: (.bandit
)
[bandit] targets: /app exclude: /test skips: B102 tests: B101, B301
※テストのIDは、Bandit repositoryのREADMEに書いてあります
- 除外設定
- Banditでは警告が出るが、その行がレビューされ、偽陽性・許容可能である場合に
#nosec
というコメントを対象の行につけることで、banditのレポート対象外になります。
- 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.RSA
や pyCrypto.PKCS1_OAEP
など、 pyCryptモジュール自体がもはや活発にメンテナンスされていないので、代わりに pyca/cryptography
を使いなさいとのこと。 RSAのサンプルコードといえば pyCrypto
というくらい出回っているので、こういうツールがないとなかなか気づきにくそう。
以下、セキュリティ勉強会のために作った脆弱性のあることがわかっているWebApplicationのソースを解析した結果です。terminal上では色付きで結果を表示してくれて十分見やすいです。
ちなみに、-f html
オプションを付けて実行すると、下記のような出力が得られます。
Pyt
Pytとは
公式のREADMEより
特徴:
- コマンドインジェクション
- SSRF
- SQLインジェクション
- XSS
- ディレクトリトラバル
などの検出
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なのでオプション無しで実行します。
テスト対象の設定ファイルをカスタマイズ・設定します
- defaultではこちらが使われます
- 対象のmethodの取捨選択や、チェック内容をカスタマイズできます
検出したい関数をカスタマイズする
- defaultではこちらが使われます
上記のように、ターゲット対象・対象外の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つ目は name
か password
かの違いなので、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に導入してみませんか?
参考リンク
- GitHub - PyCQA/bandit: Bandit is a tool designed to find common security issues in Python code.
- GitHub - python-security/pyt: A Static Analysis Tool for Detecting Security Vulnerabilities in Python Web Applications
- 開発工程に脆弱性検査の組み込みを、クラウドサービスで実現するHP:カジュアルに作れるからこそ気にしたい、Webアプリの脆弱性 - @IT
- Developers Summit 2017 Summer【C-6】CIにおけるセキュリティテストの組み込み方について 聴講メモ - dimeizaのブログ
- GitHub - kusuwada/security_handson: for Security Hanson.
- セキュリティ勉強会のために作った脆弱性のあることがわかっているWebApplicationのソース