結論: SQLインジェクションを防ぐ**根本的な対策は、プレースホルダ(パラメータ化クエリ)**です。エスケープや入力チェックは、補助にすぎず、それだけに頼ると、漏れが出やすくなります。そのうえで、データベースの権限を絞るなど、被害を小さくする対策も、重ねます。
SQLインジェクションの仕組みや被害は、SQLインジェクションとはで解説しています。この記事は、対策の具体的な方法に絞って解説します。
対策の優先順位
| 順位 | 対策 | 位置づけ |
|---|---|---|
| 1 | プレースホルダ(パラメータ化クエリ) | 根本的な対策 |
| 2 | ORMなどの、標準機能を正しく使う | 根本的な対策の、一つの形 |
| 3 | プレースホルダが使えない箇所を、許可リストで限定 | 補完 |
| 4 | エスケープ処理 | 補助(漏れやすい) |
| 5 | 入力値のチェック | 補助(それだけでは不十分) |
| 6 | データベースの権限を絞る・詳細なエラーを出さない | 被害の軽減 |
対策1:プレースホルダを使う
危険な書き方
入力された値を、文字列として連結して、SQL文を作る書き方です。
query = "SELECT * FROM users WHERE name = '" + name + "'"
cursor.execute(query)
この書き方では、入力値の中に、SQLとして意味を持つ文字が含まれると、「データ」が「命令」として解釈されてしまいます。
たとえば、name に ' OR '1'='1 という文字列が入ると、条件が常に真になり、意図しない情報が取り出されるおそれがあります。
安全な書き方
SQLの文と、値を、別々に渡します。
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))
? の部分が、プレースホルダです。値は、データベース側で、あくまで「値」として扱われるため、入力に特殊な文字が含まれていても、命令として解釈されません。
プレースホルダの書き方(? や %s など)は、使うデータベースや、ライブラリによって異なります。公式のドキュメントで確認してください。Pythonの基本は、Pythonとはをご覧ください。
対策2:ORMを正しく使う
ORM(オーム)は、プログラムの言葉で、データベースを操作できる仕組みです。通常の使い方では、内部でプレースホルダが使われます。
注意: ORMでも、生のSQL文を、文字列で連結して実行する機能を使うと、危険です。ORMの標準的な書き方を、守りましょう。ORMでよく起こる、性能の問題については、N+1問題とはで解説しています。
対策3:プレースホルダが使えない場所
プレースホルダで渡せるのは、基本的に「値」です。次のような部分には、使えません。
- テーブル名
- 列名
- 並び順の指定(ASC・DESC)
許可リストで限定する
使ってよい値を、あらかじめ決めておき、入力がその中にある時だけ、使います。
allowed = {"name", "created_at"}
if sort_key not in allowed:
sort_key = "created_at"
query = f"SELECT * FROM users ORDER BY {sort_key}"
入力された文字列を、そのまま使わず、許可した値だけを、SQLに組み込むのが、ポイントです。
対策4:エスケープの限界
エスケープは、SQLで意味を持つ特殊な文字を、無害な形に変換する処理です。
- データベースごとに、必要な処理が異なる
- 文字コードの扱いなどで、漏れが出やすい
- 処理を忘れた1か所が、弱点になる
そのため、エスケープは、プレースホルダが使えない場合の補助と考え、主役にはしません。
対策5:入力チェック(サニタイズ)の効果と限界
効果がある場面
- 数値のはずの入力が、数字だけで構成されているか確認する
- 日付や、決まった形式の入力を、形式で検証する
こうした型や形式のチェックは、不正な入力を、早い段階で除外できます。
限界
- 「怪しい文字を、取り除く」方式(ブラックリスト)は、想定外の入力で、すり抜けられやすい
- 氏名やコメントのように、自由な文字列は、記号を含むことが正当
- チェックの漏れが、そのまま、弱点になる
入力チェックは、他の対策と組み合わせる、補助的な防御です。