Skip to main content

Pythonにおけるassertの危険性

著者
Headshot of Dhruv Patel

Dhruv Patel

hero assert in python

2022年8月18日

0 分で読めます

Pythonコードのバグを見つける方法はいろいろあります。組み込みデバッガー(pdb)、十分な数のユニットテスト、PycharmやVisual StudioなどのIDEに搭載されたデバッガー、try/catch文、if/else文、assert statements、あるいは、時代遅れと言われるほどコードベースの隅々にprint()文を入れるという、昔ながらの方法もあります。

assert文を使うと、バグをすばやく検出できます。また、大量のprint文を使うよりもコードへの影響を抑えられます。しかし、print文とは異なり、assert文には思いがけないリスクがあります。

この記事では、assertを安全に使う方法と、危険な使い方につながる原因を解説します。読み終えるころには、セキュリティ上の問題を招くことなく、assertを最適に活用する方法がわかります。

Pythonのassert文を使う

assertの使い方は簡単です。assert文は単純文なので、Pythonコードの1行に記述できます。

assert文の簡単な例を見てみましょう。

assert expression

上記の文では、ブール値を確認するのと同様に、式の評価結果がTrueであることを検証しています。

式がFalseの場合、例外が発生します。たとえば、次のようになります。

>>> assert 'hi' == 'there'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

スタックトレースを確認しやすくするために、AssertionErrorに含めるアサーションメッセージを指定できます。複雑なデバッグの際に役立ちます。

>>> assert 'hi' == 'there', 'Values do not match!'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: Values do not match!

assert文を使う場合、Python組み込みの__debug__変数はTrueに設定されている必要があります。Falseに設定されていると、実行時にassert文は実行されません。これは重大な問題です。Pythonを最適化モードで実行すると(本番環境では一般的に、PYTHONOPTIMIZEコマンドラインフラグ-Oを設定します)、__debug__変数がFalseに設定され、assert文が無効になります。その結果、プロジェクトに意図せず脆弱性が持ち込まれる可能性があります。詳しくは後ほど説明します。

assertは正しく使えば、デバッグに役立つ強力なツールです。しかし、使い方が複雑になるほど、開発中にセキュリティ上の脆弱性を持ち込む余地が生まれます。

要約 — assert文はPythonアプリケーションのデバッグに非常に便利ですが、通常のエラー処理や制御フローには使えません。次のいずれかが起きるためです。

  • 例外を発生させてアプリケーションを停止し、本番環境の障害を引き起こす可能性がある。

  • デバッグモードが無効で、Pythonランタイムがassert文を黙ってスキップした場合、意図しない副作用(脆弱性を含む可能性があります)がアプリケーションに発生する。

assertを安全に使う

単利を計算するサンプルアプリケーションを使って、アサーション文でプログラムをデバッグする方法を見ていきましょう。

まず、安定版のPythonをインストールします。次に、フォルダーを開き、safe_assert_example.pyという名前のPythonファイルを作成します。サポートされている任意のコードエディターを使用できます。

次のコードをsafe_assert_example.pyに追加します。

def simpleInterest(p, t, r):
print('The principal is', p)
assert isinstance(p, int) and p > 0, "Principal must be a positive integer"
print('The time period is', t)
assert isinstance(t, int) and t > 0, "Time must be a positive integer"
print('The rate of interest is', r)
assert isinstance(r, int) and r > 0, "Rate of interest must be a positive integer"
simpleInterest = (p * t * r) / 100
print('The Simple Interest is', simpleInterest)
return simpleInterest

simpleInterest(3, 5, 8)

このプログラムではsimpleInterest関数を定義し、元金、期間、利率を使って単利を計算します。

利息を正しく計算するには、元金、期間、利率に正の整数を入力する必要があります。この例ではassert文を使って、p、t、rの値が正の整数かどうかを確認します。値が正の整数でない場合、プログラムは内容を説明するエラーメッセージを返します。

実際に試してみましょう。次のコマンドでプログラムを実行します。

python safe_assert_example.py

以下のように、単利が1.2と表示されます。

PS E:\Sample\Extra\Assert Demo> python safe_assert_example.py
The principal is 3
The time period is 5
The rate of interest is 8
The Simple Interest is 1.2

このように、プログラムは問題なく実行され、エラーメッセージも表示されません。

では、無効な入力で実行するとどうなるか見てみましょう。tの値を5から文字列「time」に変更してみてください。

simpleInterest(3, "time", 8)

次に、以下のコマンドでプログラムを再実行します。

python safe_assert_example.py

今度は、次のエラーが表示されます。

PS E: \Sample\Extra\Assert Demo» python safe assert example.py
The principal is 3
The time period is time
Traceback (most recent call last):
    File "E: \Sample\Extra\Assert Demo\safe assert example.py", line 12, in <module> simpleInterest(3, "time", 8)
    File "E: \Sample\Extra\Assert Demo\safe assert example.py", line 5, in simpleInterest assert isinstance(t, int) and t > 0, "Time must be a positive integer"
AssertionError: Time must be a positive integer

この出力にはAssertionErrorが含まれています。この例外から、assert文が意図したとおりに機能し、無効な関数引数を検出したことがわかります。

この例では、アサーションによってプログラムのデバッグができただけでなく、アサーションが失敗した時点でプログラムの実行も停止しました。

最適化モードでプログラムを実行すると(PYTHONOPTIMIZEコマンドラインフラグ-Oを設定すると)、assert文が黙って無効になることを思い出してください。この動作がアプリケーションにどう影響するか見てみましょう。

tの値を「time=」のままにして、次のコマンドで単利計算プログラムを最適化モードで実行します。

python -O safe_assert_example.py

次の結果が返されます。

PS E:\Sample\Extra\Assert Demo› python -0 safe assert example.py
The principal is 3
The time period is time
The rate of interest is 8
Traceback (most recent call last):
    File "E:\Sample\Extra\Assert Demo\safe assert example.py", line 12, in <module> simpleInterest(3, "time", 8)
    File "E:\Sample\Extra\Assert Demo\safe assert example.py", line 8, in simpleInterest simpleInterest = (p * t * r)/100
TypeError: unsupported operand type(s) for /: 'str' and 'int'

関数の引数が無効であるにもかかわらず、上記の出力には予想していたAssertionErrorが含まれていません。代わりにPythonはTypeErrorを出力しています。これは、整数型と文字列型の変数を掛け合わせようとしたためです。assert文が実行されなかったことが原因です。次のような代替策を検討しましょう。

  • 型ヒントを追加して、コードの実行前にsimpleInterestへの不正な呼び出しを検出しやすくする。

  • 実行時に型を確認するには、アサーションの代わりにif文を使う。

assertを危険な方法で使う

assertはテストとデバッグにのみ使用し、本番環境では使わないでください。assert文は__debug__変数がTrueの場合にしか実行されないため、Pythonインタープリターによって無効化されることがあります。

上の例で示したとおり、Pythonを-Oで最適化モードにして実行すると、__debug__がFalseに設定されるため、assert文は機能しません。

そのため、本番環境で実行する必要がある処理にassert文を使うのは安全ではありません。たとえば、トークンやユーザー入力の検証にassertを使ってはいけません。本番環境ではこれらの文が実行されないため、トークンやユーザー入力が検証されず、アプリケーションに脆弱性が持ち込まれるおそれがあります。

本番環境で検証にassertを使うことの重大さを示すために、ユーザーの配列を受け取り、アクセスレベルに応じて先に進めるかどうかを判断するユーザー認可関数を考えてみましょう。プログラムはまずassertを使って、リストまたは配列がnullではなく、リストのインスタンスであることを確認します。その後、ロールに基づいて、管理者にアプリケーションへの完全なアクセスを許可します。ユーザーリストにadminロールがあれば、アサーションエラーは発生しません。しかし、リストにadminロールが見つからない場合はエラーが発生します。

次に、このコードをunsafe_assert_example.pyという名前のファイルに貼り付けます。

def authorize_admin_user(user_roles):
    assert isinstance(user_roles,list) and user_roles != [], "No user roles found"

    assert 'admin' in user_roles, "No admin role found."
    print("You have full access to the application.")

authorize_admin_user(['admin','user'])

authorize_admin_user関数はユーザーのロールを確認します。adminであれば、アプリケーションへのフルアクセスがあることを示します。通常、ユーザーのロールはデータベースから読み込まれます。その中にadminがあれば、ユーザーにはアプリへのフルアクセスが許可されます。そうでなければ、assertが失敗して例外が発生します。

ファイルの最後の行ではauthorize_admin_user関数を呼び出し、adminとuserの2つのロールを含むリストを渡しています。

Run python unsafe_assert_example.py:

$ python unsafe_assert_example.py
You have full access to the application

予想どおり、ユーザーにはアプリケーションへのフルアクセスが許可されます。

次に、リストにadminロールを含めずにユーザーロール関数を呼び出してみましょう。unsafe_assert_example.pyの最後の行を次のように変更します。

authorize_admin_user(['user'])

プログラムを実行し、出力を確認します。

$ python unsafe_assert_example.py
Traceback (most recent call last):
  File "C:\Projects\ContentLabEditing\python\unsafe_assert_example.py", line 6, in <module>
    authorize_admin_user(['user'])
  File "C:\Projects\ContentLabEditing\python\unsafe_assert_example.py", line 3, in authorize_admin_user
    assert 'admin' in user_roles, "No admin role found."
AssertionError: No admin role found.

予想どおり、ロールのリストにadminが含まれていないため、関数はAssertionErrorを発生させます。

次に、同じプログラムを最適化モードで実行します。

python -O unsafe_assert_example.py

出力は以下の画像のようになります。リストにadminロールがないにもかかわらず、assertが実行されなかったため、管理者認可が成功しています。

PS E:\Sample\Extra\Assert Demo> python -0 unsafe_assert_example.py
You have full access to the application
You have full access to the application
PS E:\Sample\Extra\Assert Demo>

さらに、同じ最適化モードで整数のリストを渡して関数を呼び出しても、assertは検証ではなくデバッグにのみ使用すべきであることがわかります。

関数呼び出しをauthorize_admin_user([1,2])に変更し、同じコマンドpython -O unsafe_assert_example.pyで実行します。

プログラムの出力は変わりません。

単利計算の例では、assert文が実行されなかった場合の影響は軽微でした。期待したエラーメッセージは表示されませんでしたが、Pythonのエラーメッセージから問題の原因を把握できました。さらに重要なのは、安全なデバッグの目的でassert文を使っていたため、脆弱性を持ち込まずに済んだことです。

この例ではassertを危険な方法で使い、管理者ではないユーザーに管理者アクセスを許可して、Pythonアプリケーションを脆弱な状態にしてしまいました。

これらの例をさらに試してみたい場合は、この記事で使用したすべてのコードをこちらのリポジトリで確認できます。

assertの使用上の注意点

assertを使えば、少ないコードでPythonコードを簡単にデバッグできます。ただし、安全に使わないと、アプリケーションに意図せずセキュリティ上の問題を持ち込むおそれがあります。

本番環境でassertを無効にすると、深刻な事態を招く可能性があります。この運用によって、悪意ある攻撃者に悪用されるバックドアやブレークポイントがアプリケーションに生じるおそれがあります。

assertの主な役割はバグの根本原因を特定することであり、通常のエラー処理に使ってはいけません。assertは便利ですが、デバッグやテストに使い、プログラムやアプリケーションのロジックの制御には使わず、安全な状況に限って使用することが重要です。assertの代替手段としては、ユニットテスト、デバッガー、条件分岐、バリデーターのほうが適切で安全です。

assertを安全に使う方法がわかったところで、Pythonのベストプラクティス・チートシートを読んで、あらゆるプラットフォームや環境でアプリケーションを安全に保つ方法を学びましょう。

Pythonの開発とセキュリティに関するその他のリソース

無料のオンラインPythonコードチェッカーもお試しください。Snyk Codeエンジンが、コードのセキュリティと品質に関する問題をどのように分析するか確認できます。

Capture the Flagを始めよう

オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。