Data verification: Build a secure line of defense for Flask user interaction

📂 Stage: Stage 2 - Interaction and Data (Core) 🔗 Related chapters: Flask-WTF 插件 · 密码安全加密

The data submitted by users is the first-hand material received by web applications from the outside world. Null pointer crash, XSS injection, SQL injection vulnerability, business logic error are often due to the lack of strict data verification. Instead of checking later, it is better to establish the rules at the entrance.

Today we will use the Flask-WTF extension to see how to quickly build general rules through built-in validators, then cover business-specific needs through custom validators, and finally use HTML5 native and AJAX to make the verification experience more friendly.


1. Getting started with WTForms validator core

The validation logic of WTForms is written in each fieldvalidatorsIn the list, execute in order. Once a validator throwsValidationError, the subsequent validator will no longer run, and the error message will be added to the corresponding field.errorsOn properties.

1.1 High-frequency built-in validator cheat sheet

ValidatorApplicable scenariosCore differences
DataRequiredRequired text, password, drop-down selectionStrings containing only spaces (thanInputRequiredCloser to actual needs)
EmailEmail format verificationDependenciesemail-validatorLibrary (Flask-WTF will install automatically)
Length(min, max)Length limits for text, URL aliases, and introductionsYou can only setminOr just setmax
NumberRange(min,max)Numeric input (withIntegerField / DecimalField)Supports integer and decimal ranges
EqualTo(fieldname)Confirm password, confirm email scenarioField names are case-sensitive
Regexp(pattern)Mobile phone number, user name, ID number, etc. Fixed format textSupportedreModifiers for the library (optional parameter)
OptionalOptional field, allowing to skip subsequent verification if it is emptyMust be placed at the front of all validators

2. Commonly used built-in validator combinations in practice

2.1 Basic form (general content management)

Below is an example of an article submission form that covers a combination of validations for text, URL alias, age, and blog link.

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, IntegerField, TextAreaField
from wtforms.validators import (
    DataRequired, Email, Length, EqualTo,
    Regexp, URL, NumberRange, Optional
)


class ArticleSubmissionForm(FlaskForm):
    # 标题:必填、5-100 字符、仅允许中文/英文/数字/下划线/空格
    title = StringField(
        "投稿标题",
        validators=[
            DataRequired(message="标题可不能空着哦😯"),
            Length(
                min=5, max=100,
                message="标题太长太短都不行,请控制在 5-100 个字符之间"
            ),
            Regexp(
                r"^[\u4e00-\u9fa5a-zA-Z0-9_ ]+$",
                message="标题只能包含中文、英文字母、数字、下划线和空格"
            ),
        ],
    )

    # URL 别名:必填、5-50 字符、仅允许小写字母/数字/连字符(SEO 友好)
    url_slug = StringField(
        "URL 别名(用于生成链接)",
        validators=[
            DataRequired(),
            Length(max=50, message="URL 别名太长啦,不利于分享"),
            Regexp(
                r"^[a-z0-9-]+$",
                message="只能用小写字母、数字和连字符哦"
            ),
        ],
    )

    # 作者年龄:可选、18-100 岁
    author_age = IntegerField(
        "您的年龄(选填)",
        validators=[
            Optional(),
            NumberRange(
                min=18, max=100,
                message="如果填写年龄,请确保是 18-100 岁之间的有效数字"
            ),
        ],
    )

    # 个人博客:可选、仅允许 http/https 开头
    personal_blog = StringField(
        "个人博客链接(选填)",
        validators=[
            Optional(),
            URL(message="请输入有效的博客链接,记得加上 http:// 或 https:// 前缀"),
        ],
    )

2.2 Exclusive scenarios for regular expressions (fixed format verification)

For rules that the built-in validator cannot handle, just useRegexpCome on:

# 中国手机号验证
phone = StringField(
    "手机号",
    validators=[
        Regexp(
            r"^1[3-9]\d{9}$",
            message="请输入 11 位有效的中国手机号"
        ),
    ],
)

# 合规用户名(字母开头、字母数字下划线、3-20 位)
username = StringField(
    "用户名",
    validators=[
        Regexp(
            r"^[a-zA-Z]\w{2,19}$",
            message="用户名需以字母开头,3-20 个字符,仅允许字母、数字和下划线"
        ),
    ],
)

3. Custom validator: covering business-specific requirements

When the built-in validators and regular expressions are not enough (for example, if you need to check the database or check sensitive words), it is time to use a custom validator.

3.1 Field-level validation (defined inside the form)

It is suitable to use the logic of **only for the current form. The method name must comply withvalidate_字段名Naming rules:

# 假设已定义 User 模型和 db 实例
# from app.models import User
# from app import db

class SignUpForm(FlaskForm):
    username = StringField("用户名", validators=[
        DataRequired(), Length(3, 20),
    ])
    bio = TextAreaField("个人简介", validators=[
        Length(max=200, message="简介最多 200 个字符"),
    ])

    # 同时检查敏感词和唯一性
    def validate_username(self, field):
        sensitive_words = ["admin", "root", "system", "test"]
        if any(word in field.data.lower() for word in sensitive_words):
            raise ValidationError("该用户名太敏感啦,换一个试试😉")

        if User.query.filter_by(username=field.data).first():
            raise ValidationError("这个用户名已经被别人注册了哦")

    # 简介里禁止出现外部链接
    def validate_bio(self, field):
        if field.data and ("http://" in field.data.lower() or "https://" in field.data.lower()):
            raise ValidationError("为了社区安全,简介中暂时禁止添加链接")

3.2 Independent custom validator (universal reuse)

Suitable for ** multiple forms that will use ** logic, put it separatelyapp/validators.pyinside:

# app/validators.py
import re
from wtforms.validators import ValidationError


# 通用敏感词检查
def no_common_sensitive_words(form, field):
    """检查文本是否包含通用敏感词(示例列表)"""
    sensitive_words = ["诈骗", "赌博", "色情"]
    if any(word in field.data for word in sensitive_words):
        raise ValidationError("文本中包含违规内容,请修改后再提交")


# 通用外部链接禁止
def no_external_links(form, field):
    """检查文本是否包含以 http/https 开头的外部链接"""
    url_pattern = r"https?://[^\s]+"
    if re.search(url_pattern, field.data, re.IGNORECASE):
        raise ValidationError("文本中暂时禁止添加外部链接")

Then import it directly into any form:

from app.validators import no_common_sensitive_words, no_external_links

class CommentForm(FlaskForm):
    content = TextAreaField("评论内容", validators=[
        DataRequired(), Length(1, 500),
        no_common_sensitive_words, no_external_links,
    ])

4. Optimize verification experience: client-assisted verification

⚠️ Core Security Principles: Client-side verification is just a "dessert" to improve the experience. What truly protects data security is server-side verification (WTForms). Attackers can easily disable JavaScript or send forged requests, so backend defense is a must.

4.1 HTML5 native verification (zero code based experience)

You can use fields generated by WTForms or manually add HTML5 attributes in Jinja2 templates:

<form method="POST" action="{{ url_for('auth.signup') }}">
    {{ form.hidden_tag() }} <!-- Flask-WTF 必须:CSRF 保护 -->

    <div>
        {{ form.email.label }}
        <!-- 手动添加 HTML5 原生属性 -->
        <input
            type="email"
            name="{{ form.email.name }}"
            id="{{ form.email.id }}"
            required
            minlength="5"
            maxlength="100"
            placeholder="your@email.com"
            {% if form.email.data %}value="{{ form.email.data }}"{% endif %}
        >
        {% if form.email.errors %}
            <span style="color: red;">{{ form.email.errors[0] }}</span>
        {% endif %}
    </div>

    <button type="submit">注册</button>
</form>

4.2 AJAX real-time verification (advanced experience)

Allow users to see feedback during the input process, reducing the frustration of reporting errors after submission.

Backend API routing:

# app/routes/api.py
from flask import Blueprint, jsonify
from app.models import User

api_bp = Blueprint("api", __name__, url_prefix="/api")


@api_bp.route("/check/username/<username>")
def check_username(username):
    exists = User.query.filter_by(username=username).first() is not None
    return jsonify({"available": not exists, "username": username})


@api_bp.route("/check/email/<email>")
def check_email(email):
    import re
    if not re.match(r"^[\w\.-]+@[\w\.-]+\.\w+$", email):
        return jsonify({"valid": False, "available": False})
    exists = User.query.filter_by(email=email).first() is not None
    return jsonify({"valid": True, "available": not exists})

Front-end JavaScript (real-time verification of username):

document.getElementById("username").addEventListener("blur", async function() {
    const username = this.value.trim();
    if (!username) return; // 为空时不发送请求

    const response = await fetch(`/api/check/username/${username}`);
    const data = await response.json();
    const errorEl = document.getElementById("username-error");

    if (!data.available) {
        errorEl.textContent = "这个用户名已经被注册啦";
        errorEl.style.color = "red";
        errorEl.style.display = "block";
    } else {
        errorEl.textContent = "这个用户名可以用哦🎉";
        errorEl.style.color = "green";
        errorEl.style.display = "block";
    }
});

5. Full text summary

  1. Verifier running rules:
  • Execute in list order and stop if any verification fails.
    • Optional()It should be placed at the front of the validator list, otherwise a null value will also trigger subsequent validation.
  • useEmailConfirm before validatoremail-validatorThe library is installed.
  1. Combined routines for typical scenarios:
  • Text class:DataRequired + Length+ optionalRegexp
  • Number Class:DataRequired / Optional + NumberRange
  • Confirmation Class:EqualToMatch the corresponding field's own rules.
  • URL/Email Class:Optional / DataRequired + URL / Email
  1. Safety first principle:
  • Server-side validation (WTForms) is the bottom line, must be written and cannot be omitted.
  • Client-side validation (HTML5/AJAX) is just the icing on the cake and in no way a replacement for back-end validation.
  • For sensitive operations such as registration, login, and payment, database uniqueness checks and additional business rules must be added.

🔗 Extended reading