Flask で SQL インジェクションを防ぐ

SQL インジェクションは、ユーザー入力を SQL クエリに直接埋め込むことで発生する脆弱性である。Flask と SQLAlchemy を使った安全なデータベース操作を解説する。

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

# 危険: ユーザー入力を直接埋め込む
@app.route('/search')
def search():
    q = request.args.get('q')
    # 攻撃者が q に "'; DROP TABLE users; --" を入力すると...
    result = db.engine.execute(f"SELECT * FROM posts WHERE title = '{q}'")
    return render_template('results.html', results=result)

攻撃者は入力を操作することで、任意の SQL を実行できてしまう。

SQLAlchemy ORM を使う

SQLAlchemy ORM はパラメータを自動的にエスケープする。

@app.route('/search')
def search():
    q = request.args.get('q')
    # 安全: ORM がパラメータをエスケープ
    posts = Post.query.filter(Post.title == q).all()
    return render_template('results.html', posts=posts)

パラメータ化クエリ

生の SQL を使う必要がある場合は、パラメータ化クエリを使う。

from sqlalchemy import text

@app.route('/search')
def search():
    q = request.args.get('q')
    # 安全: パラメータをバインド
    result = db.session.execute(
        text("SELECT * FROM posts WHERE title = :title"),
        {'title': q}
    )
    return render_template('results.html', results=result)

:title はプレースホルダで、{'title': q} で値をバインドする。

LIKE 検索の注意点

# 危険: % をそのまま埋め込む
posts = Post.query.filter(Post.title.like(f'%{q}%')).all()

# 安全: ワイルドカードをエスケープ
from sqlalchemy import func
q_escaped = q.replace('%', r'\%').replace('_', r'\_')
posts = Post.query.filter(Post.title.like(f'%{q_escaped}%', escape='\\')).all()

動的なカラム名の危険

# 危険: カラム名をユーザー入力から取る
order_by = request.args.get('sort', 'id')
posts = Post.query.order_by(order_by).all()  # SQL インジェクションの可能性
# 安全: ホワイトリストでチェック
ALLOWED_SORT = {'id', 'title', 'created_at'}
order_by = request.args.get('sort', 'id')
if order_by not in ALLOWED_SORT:
    order_by = 'id'
posts = Post.query.order_by(getattr(Post, order_by)).all()

IN 句の安全な使用

# 安全: リストをそのまま渡す
ids = [1, 2, 3]
posts = Post.query.filter(Post.id.in_(ids)).all()

SQLAlchemy が適切にエスケープしてくれる。

生の SQL を避けられない場合

from sqlalchemy import text

# 複雑なクエリでも必ずパラメータ化
query = text("""
    SELECT p.*, u.username 
    FROM posts p 
    JOIN users u ON p.user_id = u.id 
    WHERE p.status = :status AND u.role = :role
""")
result = db.session.execute(query, {'status': 'published', 'role': 'author'})

防御のまとめ

ORM を使う(最も安全)
生 SQL は必ずパラメータ化クエリ
カラム名・テーブル名はホワイトリストで検証
エラーメッセージで SQL 構造を露出しない

SQLAlchemy を正しく使えば、SQL インジェクションのリスクを大幅に減らせる。