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