PythonでSQLインジェクションを防ぐ安全なクエリの書き方
生徒
「先生、データベースを使うときに気をつけることってありますか?」
先生
「とても重要な質問ですね。データベースを扱う際は、SQLインジェクションという危険な攻撃に注意する必要があります。これは、悪意のあるSQL文を実行されてしまう脆弱性のことです。」
生徒
「そんな怖い攻撃があるんですか?どうやって防げばいいんですか?」
先生
「安心してください。正しい書き方を学べば、簡単に防ぐことができます。それでは、安全なクエリの書き方を詳しく見ていきましょう!」
1. SQLインジェクションとは?基本的な仕組み
SQLインジェクション(SQL Injection)は、Webアプリケーションにおける最も深刻なセキュリティ脆弱性の一つです。攻撃者が悪意のあるSQL文を注入(インジェクション)することで、データベースを不正に操作される攻撃手法です。
例えるなら、銀行の窓口で「田中太郎の口座から1万円引き出してください」と伝えるべきところを、「田中太郎の口座から1万円引き出してください。そして全員の口座情報を見せてください」と余計な命令を追加するようなものです。
SQLインジェクションの危険性
- データベース内の全データが盗まれる可能性
- ユーザーのパスワードや個人情報が流出する危険
- データが改ざんされたり削除されたりする恐れ
- システム全体が乗っ取られる可能性
2. 危険なコードの例:文字列連結によるSQL文の作成
まず、絶対にやってはいけない危険なコードの例を見てみましょう。以下のコードは、ユーザー入力を直接SQL文に埋め込んでいるため、SQLインジェクション攻撃に対して無防備です。
悪い例(危険なコード)
import sqlite3
# ユーザーからの入力を受け取る
user_input = input("検索したいユーザー名を入力してください: ")
connection = sqlite3.connect('users.db')
cursor = connection.cursor()
# 危険!文字列連結でSQL文を作成している
dangerous_query = "SELECT * FROM users WHERE name = '" + user_input + "'"
cursor.execute(dangerous_query)
results = cursor.fetchall()
for row in results:
print(row)
connection.close()
なぜ危険なのか?
このコードでは、ユーザーが以下のような入力をすると、データベース全体が危険にさらされます:
攻撃者の入力例:
' OR '1'='1
この入力により、実際に実行されるSQL文は以下のようになります:
SELECT * FROM users WHERE name = '' OR '1'='1'
'1'='1'は常に真(True)なので、WHERE条件が無効化され、テーブル内のすべてのユーザー情報が取得されてしまいます。これがSQLインジェクション攻撃の基本的な仕組みです。
3. 安全なコード:プレースホルダーの使用
SQLインジェクションを防ぐ最も確実な方法は、プレースホルダー(パラメータ化クエリ)を使用することです。プレースホルダーは、SQL文の中に「穴」を開けておいて、後から安全に値を埋め込む仕組みです。
正しい例(安全なコード)
import sqlite3
# ユーザーからの入力を受け取る
user_input = input("検索したいユーザー名を入力してください: ")
connection = sqlite3.connect('users.db')
cursor = connection.cursor()
# 安全!プレースホルダーを使用
safe_query = "SELECT * FROM users WHERE name = ?"
cursor.execute(safe_query, (user_input,))
results = cursor.fetchall()
for row in results:
print(row)
connection.close()
このコードでは、?をプレースホルダーとして使用しています。ユーザーの入力は、execute()メソッドの第2引数としてタプルで渡されます。これにより、どんな入力が来ても、それは単なる「データ」として扱われ、SQL文として実行されることはありません。
4. 複数のパラメータを使う安全なクエリ
複数の値をSQL文に埋め込む場合も、プレースホルダーを使うことで安全に処理できます。各プレースホルダーに対応する値を順番にタプルで渡します。
import sqlite3
# ユーザーからの入力を受け取る
search_name = input("名前を入力: ")
min_age = input("最小年齢を入力: ")
connection = sqlite3.connect('users.db')
cursor = connection.cursor()
# 複数のプレースホルダーを使用
query = "SELECT * FROM users WHERE name LIKE ? AND age >= ?"
cursor.execute(query, (f"%{search_name}%", min_age))
results = cursor.fetchall()
print(f"検索結果: {len(results)}件")
for row in results:
print(f"ID: {row[0]}, 名前: {row[1]}, 年齢: {row[2]}")
connection.close()
この例では、2つのプレースホルダーを使用しています。LIKE演算子を使った部分一致検索でも、プレースホルダーを使えば安全です。%記号は、Python側で文字列に追加しておきます。
5. データベースごとのプレースホルダーの違い
Pythonでデータベースを扱う際、使用するライブラリによってプレースホルダーの記法が異なります。それぞれの正しい書き方を理解することが重要です。
SQLite(sqlite3)の場合
SQLiteでは、クエスチョンマーク(?)をプレースホルダーとして使用します。
import sqlite3
connection = sqlite3.connect('sample.db')
cursor = connection.cursor()
# SQLiteでは ? を使用
cursor.execute("INSERT INTO users (name, email) VALUES (?, ?)",
('田中太郎', 'tanaka@example.com'))
cursor.execute("SELECT * FROM users WHERE age > ?", (18,))
connection.commit()
connection.close()
MySQL(pymysql)の場合
MySQLでは、パーセント記号とs(%s)をプレースホルダーとして使用します。
import pymysql
connection = pymysql.connect(
host='localhost',
user='root',
password='password',
database='test_db'
)
cursor = connection.cursor()
# MySQLでは %s を使用
cursor.execute("INSERT INTO users (name, email) VALUES (%s, %s)",
('佐藤花子', 'sato@example.com'))
cursor.execute("SELECT * FROM users WHERE name = %s", ('田中太郎',))
connection.commit()
connection.close()
PostgreSQL(psycopg2)の場合
PostgreSQLでも、%sをプレースホルダーとして使用します。
import psycopg2
connection = psycopg2.connect(
host='localhost',
database='sample_db',
user='postgres',
password='password'
)
cursor = connection.cursor()
# PostgreSQLでも %s を使用
cursor.execute("INSERT INTO users (name, email) VALUES (%s, %s)",
('鈴木一郎', 'suzuki@example.com'))
cursor.execute("UPDATE users SET age = %s WHERE name = %s", (30, '佐藤花子'))
connection.commit()
connection.close()
%sは、Pythonの文字列フォーマットの%sとは異なります。データベースライブラリが提供する専用のプレースホルダーなので、文字列の型に関係なく%sを使用します。
6. INSERT・UPDATE・DELETE文での安全な書き方
データの追加、更新、削除を行う際も、必ずプレースホルダーを使用してください。これらの操作でSQLインジェクションが発生すると、データが破壊される危険性があります。
import sqlite3
connection = sqlite3.connect('users.db')
cursor = connection.cursor()
# ユーザー登録(INSERT)
new_name = input("登録する名前: ")
new_email = input("メールアドレス: ")
new_age = input("年齢: ")
cursor.execute(
"INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
(new_name, new_email, new_age)
)
print("ユーザーを登録しました")
# ユーザー情報の更新(UPDATE)
update_name = input("更新するユーザー名: ")
new_age = input("新しい年齢: ")
cursor.execute(
"UPDATE users SET age = ? WHERE name = ?",
(new_age, update_name)
)
print(f"{cursor.rowcount}件のデータを更新しました")
# ユーザーの削除(DELETE)
delete_name = input("削除するユーザー名: ")
cursor.execute(
"DELETE FROM users WHERE name = ?",
(delete_name,)
)
print(f"{cursor.rowcount}件のデータを削除しました")
connection.commit()
connection.close()
どの操作でも、ユーザー入力を直接SQL文に埋め込まず、プレースホルダーを経由して渡しています。これにより、悪意のある入力からデータベースを守ることができます。
7. やってはいけないこと:文字列フォーマットの使用
Pythonの文字列フォーマット機能(f-string、format()、%演算子)を使ってSQL文を作成することは、絶対に避けてください。これらは、SQLインジェクション対策にはなりません。
危険な例(絶対にやってはいけない)
import sqlite3
user_input = input("ユーザー名: ")
connection = sqlite3.connect('users.db')
cursor = connection.cursor()
# 危険!f-stringを使用(SQLインジェクションの危険あり)
cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'")
# 危険!format()を使用(SQLインジェクションの危険あり)
cursor.execute("SELECT * FROM users WHERE name = '{}'".format(user_input))
# 危険!%演算子を使用(SQLインジェクションの危険あり)
cursor.execute("SELECT * FROM users WHERE name = '%s'" % user_input)
connection.close()
これらの方法は、文字列を単純に連結しているだけなので、SQLインジェクション攻撃を防ぐことができません。必ずデータベースライブラリが提供するプレースホルダー機能を使用してください。
8. 入力値の検証とバリデーション
プレースホルダーを使用することに加えて、ユーザー入力の検証(バリデーション)を行うことで、さらにセキュリティを強化できます。
入力値の検証例
import sqlite3
def is_valid_username(username):
"""ユーザー名が有効かチェックする関数"""
# 文字数チェック(3文字以上20文字以内)
if len(username) < 3 or len(username) > 20:
return False
# 使用可能な文字チェック(英数字とアンダースコアのみ)
allowed_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_')
if not all(char in allowed_chars for char in username):
return False
return True
# ユーザー入力を受け取る
user_input = input("ユーザー名を入力: ")
# 入力値を検証
if not is_valid_username(user_input):
print("エラー: ユーザー名は3~20文字の英数字とアンダースコアのみ使用できます")
else:
connection = sqlite3.connect('users.db')
cursor = connection.cursor()
# 検証に合格した値をプレースホルダーで使用
cursor.execute("SELECT * FROM users WHERE username = ?", (user_input,))
result = cursor.fetchone()
if result:
print(f"ユーザーが見つかりました: {result[1]}")
else:
print("ユーザーが見つかりませんでした")
connection.close()
入力値の検証を行うことで、予期しない値がデータベースに渡されることを防ぎます。ただし、検証だけではSQLインジェクションを完全には防げないため、必ずプレースホルダーと併用してください。
9. ORMを使った安全なデータベース操作
ORM(Object-Relational Mapping)ツールを使用すると、SQL文を直接書かずにデータベース操作ができます。ORMは内部で自動的にプレースホルダーを使用するため、SQLインジェクションのリスクが大幅に減少します。
主要なPython ORM
- SQLAlchemy:最も人気のあるPython ORM
- Django ORM:Djangoフレームワークに組み込まれたORM
- Peewee:軽量でシンプルなORM
10. セキュリティのベストプラクティス
SQLインジェクションを防ぐために、押さえておくべき重要なポイントをまとめました。
必ず守るべきルール
- 常にプレースホルダーを使用する(
?または%s) - ユーザー入力を直接SQL文に埋め込まない
- 文字列の連結やフォーマット機能でSQL文を作成しない
- f-string、
format()、%演算子をSQL文に使わない
追加の対策
- 入力値の検証(バリデーション)を実装する
- データベースユーザーの権限を最小限に設定する
- エラーメッセージでデータベースの詳細を表示しない
- 定期的にセキュリティテストを実施する
- ライブラリとフレームワークを最新版に保つ
コードレビューのチェックポイント
- すべてのSQL文でプレースホルダーが使われているか
- ユーザー入力が適切に検証されているか
- データベース接続情報が安全に管理されているか
- エラー処理が適切に実装されているか
開発者への重要なメッセージ
SQLインジェクション対策は、データベースを扱うすべての開発者の責任です。プレースホルダーを使うという単純なルールを守るだけで、深刻なセキュリティ被害を防ぐことができます。
「自分のアプリは大丈夫」と思わず、すべてのコードでセキュリティを意識してください。ユーザーの大切なデータを守るため、常に安全なコーディングを心がけましょう。