ユーザーからの入力を信頼せず、適切にバリデーションとサニタイズを行うことは、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 フィルタは、サニタイズ済みのデータにのみ使用すること。
入力バリデーションのベストプラクティス
ホワイトリスト方式で許可する文字・形式を定義
長さ、範囲、形式を必ずチェック
クライアント側バリデーションは信用しない
エラーメッセージで内部情報を漏らさない
適切なバリデーションとサニタイズにより、多くの攻撃を未然に防ぐことができる。