Flask の入力バリデーションとサニタイズ

ユーザーからの入力を信頼せず、適切にバリデーションとサニタイズを行うことは、Flask アプリケーションのセキュリティに不可欠である。

バリデーションとサニタイズの違い

バリデーション入力が期待する形式か検証し、不正なら拒否
サニタイズ入力から危険な部分を除去・エスケープして安全にする

両方を組み合わせることが重要である。

Flask-WTF によるフォームバリデーション

pip install flask-wtf email-validator
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, IntegerField
from wtforms.validators import DataRequired, Email, Length, NumberRange

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[
        DataRequired(),
        Length(min=3, max=20)
    ])
    email = StringField('Email', validators=[
        DataRequired(),
        Email()
    ])
    password = PasswordField('Password', validators=[
        DataRequired(),
        Length(min=8)
    ])
    age = IntegerField('Age', validators=[
        NumberRange(min=0, max=150)
    ])
@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        # バリデーション済みのデータ
        username = form.username.data
        return redirect(url_for('login'))
    return render_template('register.html', form=form)

カスタムバリデータ

from wtforms.validators import ValidationError

def username_exists(form, field):
    if User.query.filter_by(username=field.data).first():
        raise ValidationError('Username already taken')

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[
        DataRequired(),
        username_exists
    ])

API のバリデーション(marshmallow)

pip install marshmallow
from marshmallow import Schema, fields, validate, ValidationError

class UserSchema(Schema):
    username = fields.Str(required=True, validate=validate.Length(min=3, max=20))
    email = fields.Email(required=True)
    age = fields.Int(validate=validate.Range(min=0, max=150))

@app.route('/api/users', methods=['POST'])
def create_user():
    schema = UserSchema()
    try:
        data = schema.load(request.json)
    except ValidationError as err:
        return {'errors': err.messages}, 400
    
    # バリデーション済みの data を使用
    user = User(**data)
    db.session.add(user)
    db.session.commit()
    return schema.dump(user), 201

HTML のサニタイズ

ユーザー入力を HTML として表示する場合、XSS を防ぐためにサニタイズが必要である。

pip install bleach
import bleach

# 許可するタグと属性を指定
ALLOWED_TAGS = ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li']
ALLOWED_ATTRS = {'a': ['href', 'title']}

def sanitize_html(dirty_html):
    return bleach.clean(
        dirty_html,
        tags=ALLOWED_TAGS,
        attributes=ALLOWED_ATTRS,
        strip=True
    )

@app.route('/post', methods=['POST'])
def create_post():
    content = request.form['content']
    safe_content = sanitize_html(content)
    post = Post(content=safe_content)
    db.session.add(post)
    db.session.commit()
    return redirect(url_for('index'))

Jinja2 の自動エスケープ

Jinja2 はデフォルトで HTML エスケープを行う。

<!-- 安全: 自動エスケープ -->
<p>{{ user_input }}</p>

<!-- 危険: エスケープを無効化 -->
<p>{{ user_input | safe }}</p>

| safe フィルタは、サニタイズ済みのデータにのみ使用すること。

入力バリデーションのベストプラクティス

ホワイトリスト方式で許可する文字・形式を定義
長さ、範囲、形式を必ずチェック
クライアント側バリデーションは信用しない
エラーメッセージで内部情報を漏らさない

適切なバリデーションとサニタイズにより、多くの攻撃を未然に防ぐことができる。