この記事は 「Pythonって、たぶんそういう意味じゃない」シリーズ と
「誰でもわかるセキュリティの話」シリーズ の両方に属する記事です。
Python歴5年以上、メインで使い続けてきた変態が書いています。
この記事は 個人の経験に基づくポエムです。
環境・用途・経験によって異なる場合があります。
Discordの電卓ボットを作っていた。
ユーザーが2^3+1と入力したら答えを返す。シンプルな機能だ。
実装はこうした。
expr = user_input.replace("^", "**")
result = eval(expr)
^を**に変換して、あとはeval()に投げるだけ。
動く。シンプル。最高じゃないか。
「使うな」とよく言われるけど、なぜかはあまり考えていなかった。
サーバー内だけで使うボットだから、まあいいかと思っていた。
eval()は何をするのか
result = eval("1 + 2 * 3")
print(result) # 7
文字列をPythonのコードとして実行する。
動的に式を評価したいとき、これが手っ取り早い。
「数式を文字列で受け取って計算したい」というケースに、ぴったりはまる。
なぜ「使うな」と言われるのか
問題は何でも実行できるという点だ。
# こんな入力が来たら?
eval("__import__('os').system('rm -rf /')")
計算式のつもりで受け取った文字列が、
システムコマンドの実行になる。
電卓ボットに2+2ではなく、これを入力されたら——
サーバーのファイルが消える。
「ユーザーの入力をそのままeval()に渡す」は致命的なセキュリティホールになる。
「サーバー内だけだからいい」は本当か
自分もそう思っていた。
身内だけのDiscordサーバーなら問題ないんじゃないか、と。
でも考えてみると——
- サーバーメンバーが増える可能性がある
- コードを他のプロジェクトに流用するかもしれない
- 悪意がなくても変な入力をする人はいる
「今は安全」は「ずっと安全」じゃない。
じゃあどうするか
数式の評価なら、より安全な代替手段がある。
入力を数式のみに制限する
import re
def safe_eval(expr):
# 数字と演算子以外を弾く
if not re.match(r'^[\d\s\+\-\*\/\.\(\)\*\^]+$', expr):
raise ValueError("無効な入力です")
expr = expr.replace("^", "**")
return eval(expr)
ast.literal_eval()を使う
リテラルのみ評価するので任意コードは実行できない。
ただし数式の評価はできないので用途が限られる。
sympyやnumexprを使う
数式評価に特化したライブラリ。
任意コードの実行リスクがなく、複雑な計算式も扱える。
- どうしても
eval()→ 入力を正規表現で厳密に制限する - 数式評価に特化 →
sympyやnumexpr - リテラルのみ →
ast.literal_eval()
まとめ
電卓ボットにeval()を使うこと自体は間違いじゃない。
でも入力を信頼しすぎるのが問題だ。
「使うな」の本当の意味は「入力を検証せずに使うな」だと思っている。
「Pythonって、たぶんそういう意味じゃない」 シリーズでは、
こういう"なんとなくで語られがちなPython"を言語化していきます。
👉 ストックをフォローしておくと次の記事を見逃しません!
「誰でもわかるセキュリティの話」 シリーズでは、
セキュリティの仕組みを言語化していきます。
👉 ストックをフォローしておくと次の記事を見逃しません!