カテゴリ: Python 更新日: 2026/03/02

PythonでSQLインジェクションを防ぐ安全なクエリの書き方

PythonでSQLインジェクションを防ぐ安全なクエリの書き方
PythonでSQLインジェクションを防ぐ安全なクエリの書き方

先生と生徒の会話形式で理解しよう

生徒

「先生、データベースを使うときに気をつけることってありますか?」

先生

「とても重要な質問ですね。データベースを扱う際は、SQLインジェクションという危険な攻撃に注意する必要があります。これは、悪意のあるSQL文を実行されてしまう脆弱性のことです。」

生徒

「そんな怖い攻撃があるんですか?どうやって防げばいいんですか?」

先生

「安心してください。正しい書き方を学べば、簡単に防ぐことができます。それでは、安全なクエリの書き方を詳しく見ていきましょう!」

1. SQLインジェクションとは?基本的な仕組み

1. SQLインジェクションとは?基本的な仕組み
1. SQLインジェクションとは?基本的な仕組み

SQLインジェクション(SQL Injection)は、Webアプリケーションにおける最も深刻なセキュリティ脆弱性の一つです。攻撃者が悪意のあるSQL文を注入(インジェクション)することで、データベースを不正に操作される攻撃手法です。

例えるなら、銀行の窓口で「田中太郎の口座から1万円引き出してください」と伝えるべきところを、「田中太郎の口座から1万円引き出してください。そして全員の口座情報を見せてください」と余計な命令を追加するようなものです。

SQLインジェクションの危険性

  • データベース内の全データが盗まれる可能性
  • ユーザーのパスワードや個人情報が流出する危険
  • データが改ざんされたり削除されたりする恐れ
  • システム全体が乗っ取られる可能性
重要:SQLインジェクションは、2023年のOWASP Top 10(最も危険なWebアプリケーション脆弱性のランキング)で第3位にランクインしている深刻な脅威です。必ず対策を実装してください。

2. 危険なコードの例:文字列連結によるSQL文の作成

2. 危険なコードの例:文字列連結による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. 安全なコード:プレースホルダーの使用

3. 安全なコード:プレースホルダーの使用
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文として実行されることはありません。

補足:プレースホルダーを使うと、データベースライブラリが自動的に特殊文字をエスケープ(無害化)してくれます。これにより、SQL文の構造を変えるような入力が来ても安全です。

4. 複数のパラメータを使う安全なクエリ

4. 複数のパラメータを使う安全なクエリ
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. データベースごとのプレースホルダーの違い

5. データベースごとのプレースホルダーの違い
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文での安全な書き方

6. INSERT・UPDATE・DELETE文での安全な書き方
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. やってはいけないこと:文字列フォーマットの使用

7. やってはいけないこと:文字列フォーマットの使用
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. 入力値の検証とバリデーション

8. 入力値の検証とバリデーション
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を使った安全なデータベース操作

9. ORMを使った安全なデータベース操作
9. ORMを使った安全なデータベース操作

ORM(Object-Relational Mapping)ツールを使用すると、SQL文を直接書かずにデータベース操作ができます。ORMは内部で自動的にプレースホルダーを使用するため、SQLインジェクションのリスクが大幅に減少します。

主要なPython ORM

  • SQLAlchemy:最も人気のあるPython ORM
  • Django ORM:Djangoフレームワークに組み込まれたORM
  • Peewee:軽量でシンプルなORM
ヒント:本格的なWebアプリケーション開発では、ORMの使用を検討することをおすすめします。ORMを使えば、SQLインジェクション対策だけでなく、コードの保守性も向上します。

10. セキュリティのベストプラクティス

10. セキュリティのベストプラクティス
10. セキュリティのベストプラクティス

SQLインジェクションを防ぐために、押さえておくべき重要なポイントをまとめました。

必ず守るべきルール

  • 常にプレースホルダーを使用する(?または%s
  • ユーザー入力を直接SQL文に埋め込まない
  • 文字列の連結やフォーマット機能でSQL文を作成しない
  • f-string、format()%演算子をSQL文に使わない

追加の対策

  • 入力値の検証(バリデーション)を実装する
  • データベースユーザーの権限を最小限に設定する
  • エラーメッセージでデータベースの詳細を表示しない
  • 定期的にセキュリティテストを実施する
  • ライブラリとフレームワークを最新版に保つ

コードレビューのチェックポイント

  • すべてのSQL文でプレースホルダーが使われているか
  • ユーザー入力が適切に検証されているか
  • データベース接続情報が安全に管理されているか
  • エラー処理が適切に実装されているか

開発者への重要なメッセージ

SQLインジェクション対策は、データベースを扱うすべての開発者の責任です。プレースホルダーを使うという単純なルールを守るだけで、深刻なセキュリティ被害を防ぐことができます。

「自分のアプリは大丈夫」と思わず、すべてのコードでセキュリティを意識してください。ユーザーの大切なデータを守るため、常に安全なコーディングを心がけましょう。

カテゴリの一覧へ
新着記事
New1
Python
Pythonの関数を引数に渡す方法を初心者向けにやさしく解説!高階関数の使い方も完全ガイド
New2
Python
Pythonのクロージャー(Closure)とは?関数内関数とnonlocalの活用
New3
Python
Pythonのスコープとは?ローカル変数・グローバル変数の違いと使い分け
New4
Python
Pythonのreturn文の使い方!値を返す関数の書き方と注意点
人気記事
No.1
Java&Spring記事人気No1
Python
Pythonでトランザクション処理を実装する方法を完全解説!commit・rollbackで安全なSQL操作
No.2
Java&Spring記事人気No2
Python
Pythonの文字列を1文字ずつ処理する方法!for文やlist化の活用例
No.3
Java&Spring記事人気No3
Flask
FlaskでAPIのレスポンスをキャッシュ!通信負荷を軽減して爆速にする方法
No.4
Java&Spring記事人気No4
Flask
Flaskとは何か?初心者向けにできること・特徴・インストール手順までやさしく解説
No.5
Java&Spring記事人気No5
Flask
Flaskでデータベースエラーを処理する方法!初心者にもわかる例外の使い方
No.6
Java&Spring記事人気No6
Flask
FlaskのSQLAlchemyでフィルタリング検索!filter, filter_byの使い方完全ガイド
No.7
Java&Spring記事人気No7
Python
Pythonの文字列から一部を抽出する方法!スライスを使った基本的な切り出し方
No.8
Java&Spring記事人気No8
Flask
Flaskで開発中にデータベースを初期化する方法!初心者でもわかる便利なTips