開発者として、私たちは開発の初期段階からクリーンなコードを書くよう奨励されています。
同様に重要ですが、あまり語られていないのが、安全なコードを書き、使うことです。
Pythonのプロジェクトでは、すでに存在するソリューションを開発するのを避けるために、一般的にモジュールやサードパーティパッケージをインストールします。
しかし、この一般的な習慣が、ハッカーが依存関係を悪用してソフトウェアに大混乱を引き起こす理由であり、何か問題があるときに検出できるようにする必要がある理由なのです。
そのため、私たちは、Pythonプロジェクトのためのオープンソースのセキュリティ分析ユーティリティであるBanditのようなツールを使用しています。
このガイドでは、単純なコード行がどのように破壊的になりうるか、そしてBanditをどのように使えばそれを特定できるかを探っていきます。
Python のセキュリティ脆弱性
コードのセキュリティ脆弱性とは、悪意のあるエージェントがシステムやデータを搾取するために利用できる欠陥のことです。
Pythonでプログラミングしていると、関数呼び出しやモジュールのインポートに脆弱な使い方があるかもしれません。
それはローカルで呼び出されたときは安全かもしれませんが、正しい設定なしに展開されたときに、悪意のあるユーザがシステムを改ざんする扉を開くかもしれません。
日々のコーディングの中で、このような問題に出くわしたことはないでしょうか。
より一般的な攻撃や悪用は、そのような攻撃を予期している最新のフレームワークやシステムによって、ほぼ対処されています。
ここでは、そのいくつかを紹介します。
- OSコマンドインジェクション – コマンドラインユーティリティを実行し、OS関連のプロセスを呼び出すために使用する、地味な
subprocess
モジュールをベースにしています。次のスニペットはDNSのルックアップを実行するためにsubprocess
モジュールを使用し、その出力を返します。
# nslookup.py
import subprocess
domain = input("Enter the Domain: ")
output = subprocess.check_output(f"nslookup {domain}", shell=True, encoding='UTF-8')
print(output)
ここで何が問題なのでしょうか?
理想的なシナリオでは、エンドユーザーがDNSを提供し、スクリプトが nslookup
コマンドの結果を返します。
しかし、もしエンドユーザーがDNSと一緒にls
のようなOSベースのコマンドを提供した場合、次のような出力が返されます – コマンドも実行されます。
$ python3 nslookup.py
Enter the Domain: stackabuse.com ; ls
Server: 218.248.112.65
Address: 218.248.112.65#53
Non-authoritative answer:
Name: stackabuse.com
Address: 172.67.136.166
Name: stackabuse.com
Address: 104.21.62.141
Name: stackabuse.com
Address: 2606:4700:3034::ac43:88a6
Name: stackabuse.com
Address: 2606:4700:3036::6815:3e8d
config.yml
nslookup.py
コマンドの一部を渡すことを許可することで、OSレベルのターミナルにアクセスすることを許可しているのです。
もし悪意のある人が cat /etc/passwd
のようなコマンドを提供して、既存のユーザーのパスワードを明らかにしたら、事態がどれほど破壊的になるか想像してみてください。
単純に聞こえるかもしれませんが、subprocess
モジュールを使うのは非常に危険です。
- SQLインジェクション – SQLインジェクション攻撃は、広く使われているORM機能のおかげで、最近ではめったにありません。しかし、もしあなたがまだ生のSQLを使うことに固執しているなら、SQLクエリがどのように構築され、クエリパラメータがどのように安全に検証され渡されるかに注意する必要があります。
次のスニペットを考えてみましょう。
from django.db import connection
def find_user(username):
with connection.cursor() as cur:
cur.execute(f"""select username from USERS where name = '%s'""" % username)
output = cur.fetchone()
return output
この関数の呼び出しは単純で、引数として文字列、例えば "Foobar"
を渡すと、その文字列がSQLクエリに挿入され、次のような結果になります。
select username from USERS where name = 'Foobar'
しかし、前の問題と同じように、もし誰かが ;
文字を追加したら、複数のコマンドを連鎖させることができます。
例えば、'; DROP TABLE USERS;--
を挿入すると、次のような結果になります。
select username from USERS where name = ''; DROP TABLE USERS; --'
最初のコマンドは、データベースが USERS
テーブル全体を削除する直前に実行されます。
最後の引用文がダブルダッシュでコメントアウトされていることに注目してください。
SQLクエリパラメータは、適切にレビューされないと、悪夢と化す可能性があります。
ここで、セキュリティツールが、このような意図しないが有害なコード行を発見するのに役立つのです。
バンディット
Banditは Python で書かれたオープンソースのツールで、あなたの Python コードを分析し、その中の一般的なセキュリティ問題を発見するのに役立ちます。
Pythonのコードをスキャンし、前のセクションで述べたような脆弱性や悪用を発見することができます。
Banditはローカルにインストールすることもできますし、pip`を使って簡単に仮想環境にインストールすることもできます。
$ pip install bandit
Banditは、以下のような観点で利用することができます。
- DevSecOps:継続的インテグレーション(CI)プラクティスの一部としてBanditを使用する。
- 開発。開発:Banditをローカル開発の一部として使用し、開発者がコードをコミットする前に関数の実行を制御することができます。
バンディットの使用
Bandit は CI テストの一部として簡単に統合でき、コードを本番環境に出荷する前に一般的な脆弱性チェックを実行することができます。
例えば、DevSecOpsエンジニアは、プルリクエストが発生したり、コードがコミットされたりするたびにBanditを起動して、セキュリティを強化することができる。
組織のガイドラインに基づき、インポートモジュールや関数コールを許可または制限することができます。
Banditは、どのモジュールを使用し、どのモジュールをブラックリスト化するかについて、ユーザーに制御を提供します。
この制御は設定ファイルの中で定義されます。
設定ファイルは bandit-config-generator
ツールを使って生成することができます。
実行されたコードテストの出力は、CSVやJSONなどの形式でエクスポートすることができる。
設定ファイルは以下のように生成することができる。
$ bandit-config-generator -o config.yml
生成された config.yml
ファイルは、許可または取り消し可能なテスト、許可または取り消し可能な関数呼び出し、および暗号鍵の最大長に対応するいくつかの部分を含んでいる。
ユーザーはこの設定ファイルを指定してbanditを使用することもできますし、プロジェクトのディレクトリを渡すだけで全てのテストを実行することもできます。
$ bandit -r code/ -f csv -o out.csv
[main] INFO profile include tests: None
[main] INFO profile exclude tests: None
[main] INFO cli include tests: None
[main] INFO cli exclude tests: None
[main] INFO running on Python 3.8.5
434 [0.. 50.. 100.. 150.. 200.. 250.. 300.. 350.. 400.. ]
[csv] INFO CSV output written to file: out.csv
このBanditの呼び出しでは、-r
フラグでプロジェクトのディレクトリを指定し、-o
フラグで出力をCSVとして書き出します。
Banditはこのプロジェクトディレクトリ内のすべてのPythonスクリプトをテストし、その出力をCSVで返します。
出力は非常に細かく、以下のような感じです。
前のセクションで述べたように、subprocess
モジュールのインポートと shell=True
引数はセキュリティ上の脅威が高いものです。
もし、このモジュールと引数を使用することが避けられない場合は、設定ファイルでホワイトリストに登録し、B602
(subprocess_popen_with_shell_equals_true) と B404
(import_subprocess) を “skips” に含めることでテストをスキップできるようにすることが可能です。
これらのコードは生成された設定ファイルに含まれていることがあります。
skips` セクションに含まれるテストは以下の通りです。
skips: [B602, B404]
生成された設定ファイルを使用してBanditのテストを再実行すると、すべてのテストに合格したことを示す空のCSVファイルが生成されます。
> bandit -c code/config.yml -r code/ -f csv -o out2.csv
[main] INFO profile include tests: None
[main] INFO profile exclude tests: B404,B602
[main] INFO cli include tests: None
[main] INFO cli exclude tests: None
[main] INFO using config: code/config.yml
[main] INFO running on Python 3.8.5
434 [0.. 50.. 100.. 150.. 200.. 250.. 300.. 350.. 400.. ]
[csv] INFO CSV output written to file: out2.csv
組織内の共同作業のために、このBanditの設定ファイルを新しく作成したプロジェクトに埋め込んで、開発者がローカルでもアクセスできるようにする必要があります。
結論
コードはクリーンで安全であるべきです。
この短いガイドでは、Bandit を見てきました。
Bandit は、あなたがすでに使っているモジュールで、ありふれたセキュリティ問題を特定するために使われる Python ライブラリです。