【Python】evalとexecの違いを徹底解説!危険性から安全な代替案まで

Python

Pythonを学んでいると、文字列をコードとして動的に実行できるeval()exec()という関数に出会うことがあります。これらは非常に強力な機能ですが、同時に「使うのは危険」という話もよく耳にします。

この記事では、Pythonのevalexecについて、以下の点を初心者にも分かりやすく解説します。

  • evalexecの基本的な違い
  • なぜこれらが「危険」と言われるのか、具体的な理由とコード例
  • 危険性を回避するための安全な代替手段 ast.literal_eval
  • 限定的な状況で安全に使うためのヒント

この記事を読めば、evalexecを正しく理解し、セキュリティリスクのない安全なコードを書けるようになります。

はじめに:Pythonで文字列を「コード」として実行するevalとexec

eval()exec()は、文字列で書かれたPythonコードをプログラム実行中に動的に実行するための組み込み関数です。

通常、Pythonのコードはファイルに書かれたものを上から順に実行しますが、これらの関数を使うと、ユーザーの入力やファイルから読み込んだ文字列を、あたかもソースコードの一部であるかのように実行できます。

非常に便利に見えますが、この「何でも実行できる」性質が、深刻なセキュリティ上の問題を引き起こす可能性があるため、利用には細心の注意が必要です。これらの関数はPythonの多くのバージョンで利用可能です。

evalとexecの決定的な違い

一番の違いは、evalが「式」を評価して値を返すのに対し、execは「文」を実行するだけで値を返さない点です。

これだけだと少し分かりにくいので、具体的に見ていきましょう。

eval():式(Expression)を評価して「値を返す」

eval()は、引数として渡された文字列を式(Expression)として評価し、その結果を返します。式とは、1 + 1のように、評価すると何らかの値になるものです。

# 簡単な計算式
result = eval('10 * 5')
print(result)  # 出力: 50
print(type(result)) # 出力: <class 'int'>

# 文字列のリストを評価
list_str = "[1, 2, 3, 4, 5]"
my_list = eval(list_str)
print(my_list) # 出力: [1, 2, 3, 4, 5]
print(my_list[0]) # 出力: 1

# 関数呼び出しも可能
x = 10
result_func = eval('x * 10')
print(result_func) # 出力: 100

このように、evalは計算結果やオブジェクトなど、必ず戻り値を持ちます。一方で、x = 10のような代入文は「式」ではなく「文」なので、evalで実行しようとするとエラーになります。

# これはエラーになる!
# eval('y = 20')
# SyntaxError: invalid syntax

exec():文(Statement)を「実行するだけ」

exec()は、引数として渡された文字列を**文(Statement)**として実行します。文とは、変数への代入、if文、for文、関数の定義など、プログラムの動作を記述するものです。

exec()は値を返さず(常にNoneを返します)、コードを実行すること自体が目的です。

# 変数への代入文
exec('z = 100')
print(z) # 出力: 100

# 複数行のコードも実行可能
code = """
import random

for i in range(3):
    print(f"Loop {i+1}: {random.randint(1, 10)}")
"""
exec(code)
# 出力例:
# Loop 1: 5
# Loop 2: 8
# Loop 3: 1

evalではエラーになった代入文も、execなら問題なく実行できます。これが両者の最も大きな違いです。

関数対象戻り値
eval()式 (Expression)式の評価結果'1 + 1'
exec()文 (Statement)None'x = 1', 'if x > 0: ...'

Google スプレッドシートにエクスポート

なぜevalとexecは「危険」と言われるのか?

結論から言うと、外部からの入力を無防備にevalexecに渡すと、攻撃者によって意図しない任意のコードを実行されてしまう「コードインジェクション」という脆弱性が生まれるからです。

意図しないコードが実行されるリスク(コードインジェクション)

例えば、Webアプリケーションでユーザーが入力した数式を計算する機能を作るとします。

user_input = input("計算式を入力してください: ")
result = eval(user_input)
print(f"計算結果: {result}")

このコードは、10 + 20のような入力を想定していますが、もし悪意のあるユーザーが次のような文字列を入力したらどうなるでしょうか?

__import__('os').system('rm -rf /')

この文字列をevalが実行すると、osモジュールをインポートし、system関数を使ってrm -rf /という非常に危険なコマンド(Linux/macOSでルートディレクトリ以下の全ファイルを削除する)を実行しようとします。

このように、開発者が想定していない危険な処理を実行させられてしまうのが、evalexecの最大のリスクです。

実際の危険なコード例

# 警告:以下のコードは非常に危険です。絶対に実際の環境で試さないでください。
import os

# ユーザーからの入力を想定
# 例: 'os.system("ls")' のような文字列が入力された場合
malicious_input = "os.system('ls -la')" # カレントディレクトリのファイル一覧を表示するコマンド

# execで実行すると、OSコマンドが実行されてしまう
exec(malicious_input)

この例ではファイル一覧を表示するだけですが、サーバー上の重要なファイルを削除したり、外部に送信したり、システムを停止させたりするコマンドも同様に実行できてしまいます。

原則として、ユーザー入力など、信頼できないソースからの文字列をeval()exec()に渡してはいけません。

安全な代替手段:ast.literal_evalを使おう

文字列を安全にPythonのデータ構造(リストや辞書など)に戻したい場合は、ast.literal_evalを使いましょう。

ast.literal_evalとは?

ast.literal_evalは、文字列で表現されたPythonのリテラルのみを安全に評価するための関数です。リテラルとは、数値(10)、文字列('hello')、リスト([1, 2])、タプル、辞書、None, True, Falseなど、データそのものを指す記述のことです。

astモジュールに含まれているので、使う前にインポートが必要です。

import ast

# 文字列で書かれた辞書
dict_str = "{'name': 'Taro', 'age': 30}"

# ast.literal_evalで安全に辞書オブジェクトに変換
my_dict = ast.literal_eval(dict_str)

print(my_dict)        # 出力: {'name': 'Taro', 'age': 30}
print(my_dict['name']) # 出力: Taro

evalとの比較:安全な理由

ast.literal_evalは、evalと違って、コードとして実行可能なものを一切受け付けません。 変数名、関数呼び出し、計算式などを含む文字列を渡すと、エラーを発生させます。

import ast

# 変数を含む文字列 -> エラー
# ast.literal_eval('x + 1')
# ValueError: malformed node or string

# OSコマンドを含む危険な文字列 -> エラー
malicious_str = "__import__('os').system('ls')"
try:
    ast.literal_eval(malicious_str)
except ValueError as e:
    print(f"エラーが発生しました: {e}")
# 出力: エラーが発生しました: malformed node or string

このように、ast.literal_evalは評価対象を安全なリテラルに限定することで、コードインジェクションのリスクを根本的に排除しています。設定ファイルから辞書を読み込んだり、APIレスポンスの文字列をパースしたりする際に非常に有効です。

それでもeval/execが必要な限定的なケース

原則として使用は避けるべきですが、どうしても動的なコード実行が必要な場面も存在します。その場合は、細心の注意を払って利用を検討します。

信頼できるソースからの動的コード実行

例えば、アプリケーションの管理者だけが編集できる設定ファイルや、開発者が完全に管理しているデータベースからコードを読み込んで実行する場合など、入力ソースが100%信頼できると確信できる状況に限られます。

globalsとlocalsで実行環境を制限する

evalexecは、第2、第3引数としてグローバルおよびローカルの名前空間を指定できます。これにより、コードがアクセスできる変数や関数を制限し、安全性を高めることができます。

# 許可する関数だけを定義した辞書
safe_globals = {'__builtins__': {}} # 組み込み関数をすべて無効化
safe_locals = {'x': 10, 'y': 20}

# x + y は許可された変数なので実行できる
result = eval('x + y', safe_globals, safe_locals)
print(result) # 出力: 30

# open() など危険な組み込み関数は実行できない
try:
    eval("open('secret.txt')", safe_globals, safe_locals)
except NameError as e:
    print(f"エラー: {e}") # エラー: name 'open' is not defined

ただし、この方法で完全なサンドボックス(隔離環境)を構築するのは非常に難しく、抜け道も存在するため、過信は禁物です。

まとめ:evalとexecを理解して安全なコードを書こう

最後に、この記事の要点をまとめます。

  • evalは式を評価して値を返し、execは文を実行する。
  • 信頼できない外部からの入力を渡すと、コードインジェクションの危険があるため、原則として使用は避けるべき。
  • 安全な代替手段として、文字列リテラルを評価する場合はast.literal_evalを第一に検討する。

evalexecはPythonの強力な機能ですが、その力を正しく理解し、リスクを管理することが重要です。安全なコーディングを心がけ、堅牢なアプリケーションを開発しましょう。

コメント

タイトルとURLをコピーしました