Pythonの脆弱性
自分の見つけた/知った/ハマったpythonの脆弱性や使用時の注意に関する覚書。順不同、随時更新予定(最終更新2019/11/1)
動機
- 機械学習を使用したWebサービスなんかを作ろうとすると、UIの部分はReactか何かで書いて機械学習部分はpythonで、などという構成はありがちである。でもflaskとかdjangoとか適当に使ってて大丈夫だっけ? とたまに不安になることもある。
- 巷に溢れるライブラリを使用するか判断する時に、何が安全で何が危険かをあまり考えられていないので、防備もかねて、整理しておきたい。
- pythonは確かに書きやすい。この書きやすさや使用しやすさの裏にはトレードオフになっている何かがあるのではないか。
目次
- pickle
- yaml
- interactive shell
- is演算子
- try-finally
pickle
そこそこ有名。データを保存等したい時に、serializeするためにpickleを使うことはよくある。しかし出どころの不明なデータをloadすることは非常に危険である。
正常例
import pickleclass Good():
def __init__(self):
passx = Good()
p = pickle.dumps(x)
y = pickle.loads(p) # ok
問題はpickle.loadsにある。pickle.loadsはデシリアライズする対象クラスの__reduce__関数から返される関数と引数のペアを実行する。したがって、__reduce__関数をオーバーロードすると任意の関数やコマンドを実行させることができる。
異常例
import os
import pickleclass Evil():
def __reduce__(self):
return (os.system, ('rm a.txt', )) x = Evil()
p = pickle.dumps(x)
y = pickle.loads(p) # "rm a.txt" が実行される
対処法
- 不明な出処のデータをloadsしない。シリアライズされたデータをAPIとして受け付けない。
- pickleの公式ドキュメントでは、デシリアライズするオブジェクトを制限する方法が紹介されている。
pickleの脆弱性は有名で、複数の記事がすぐに見つかる。
yaml
pickleと似ている。
対処法
- 機械学習モデルとかyamlファイルとかをAPIとして受け付ける前に、それが必要なのかをよく考えて、可能な限り使わない。安全が担保されたファイルを受け付ける仕組みを作る。
- PyYAMLのsafe_load()を使用する。(しかし次の例と合わせると、どこまで安全にできるか不明)
Interactive shell
少し稀なケースかもしれないが、pythonでinteractive shellをユーザーに提供する際には非常に気をつける必要がある。試した限り、悪意のある攻撃可能性を全て消すことは不可能のように思えた。
試していないが、上のpickleやyamlの指定関数実行と組み合わせると危険そう。
攻撃例
まずは簡単な攻撃例から。
import os
os.system('rm a.txt', ) # "rm a.txt" が実行される
os.environ # 環境変数が全て表示される
対抗例
codeモジュールを使用すると、interactive shell内部で特定のワードを使用不可能にできる。
import readline, codeblack_list_words = ['import']def readfilter(*args, **kwargs):
inline = input(*args, **kwargs)
if any(map(lambda x:x in inline, black_list_words)):
print('Forbidden command')
return ""
return inlinecode.interact(banner='Restricted shell', readfunc=readfilter, local=locals()
例えば上記を実行すると、interactive shell内部でblack_list_wordsに含まれる単語を無効にできる。ここではimportを無効にしている。
Restricted shell
>>> import os
Forbidden command
>>> os
NameError: name 'os' is not defined
しかし抜け道がある。
反撃例
例えば、execを用いて文字列を分解するとimportできてしまう。
Restricted shell
>>> exec("imp" + "ort os")
>>> os
<module 'os' from '/anaconda3/envs/py3/lib/python3.6/os.py'>
対応するためには、back_list_wordsにexecを追加する必要がある。他にも、eval関数などでもimportできてしまう。
Restricted shell
>>> eval('__im' + 'port__("numpy")')
<module 'numpy' from '/anaconda3/envs/py3/lib/python3.6/site-packages/numpy/__init__.py'>
調べた限りでは、例えば他にもcompile()を使用してimport文を書き、他の関数にすげ替える方法もあるようだった。ブラックリストがいたちごっこになってしまうので、実際完全に制限できるかどうかは不明。
is演算子
これは超有名。脆弱性というよりもバグし安い箇所。結論からいえば、isはなるべく使わない。下記を見ると、何をやっているかほとんどわからないことがわかる。
例
>>> x = 100
>>> y = 100
>>> x is y
True
>>> x = 1000
>>> y = 1000
>>> x is y
False
>>> 1000 is 1000
True
対処法
isを使わず==を使用する。ちなみに上記は、isがオブジェクトのアドレスを比較していることと、pythonにおけるややこしい整数の内部表現のされ方が複合しておきている。他にも下記のようなこともある。
>>> [] is []
False
Noneとの比較や、真に使用したい時以外でisを使うのはやめた方が良い。
try-finally
これも踏みやすいバグ。finallyは、try構文を抜ける際に必ず実行される。なので、不用意にfinallyを書いてしまうと、常に実行されない文などが発生する。
異常例
def wrong_finally():
try:
some_process()
return True # ここで「抜ける」判定される
finally:
return Falsewrong_finally() # always return False
また何か見つけたら更新する。
参考リンク
読んでよかったものリスト。