← Назад к вопросам
Как валидировать значение полученное от пользователя?
1.3 Junior🔥 241 комментариев
#REST API и HTTP#Безопасность
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Валидация пользовательского ввода в Python
Валидация — это критически важная часть безопасности приложения. Я расскажу про все основные техники, от простых до продвинутых.
Принцип: "Trust nothing from users"
Никогда не доверяй пользовательским данным. Валидируй ВСЁ — на сервере, независимо от клиентской валидации.
Способ 1: Ручная валидация (базовый уровень)
def validate_email(email: str) -> bool:
"""Простая проверка email"""
if not email or not isinstance(email, str):
return False
if len(email) > 255: # Слишком длинный
return False
if '@' not in email:
return False
parts = email.split('@')
if len(parts) != 2:
return False
local, domain = parts
if not local or not domain:
return False
return True
# Использование
if validate_email(user_input):
print("Email валиден")
else:
print("Некорректный email")
Проблема: громоздко, легко ошибиться, недостаточно строго.
Способ 2: Регулярные выражения
import re
def validate_email_regex(email: str) -> bool:
"""Email валидация через regex"""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
def validate_phone(phone: str) -> bool:
"""Номер телефона: только цифры и +"""
pattern = r'^\+?[0-9\s\-()]{10,}$'
return bool(re.match(pattern, phone))
def validate_username(username: str) -> bool:
"""Username: букв, цифр, подчеркивание, дефис"""
pattern = r'^[a-zA-Z0-9_-]{3,20}$'
return bool(re.match(pattern, username))
# Использование
if validate_email_regex(user_email):
print("Email валиден")
Проблема: regex сложно читать и поддерживать. Email валидация на самом деле очень сложная.
Способ 3: Специализированные библиотеки (рекомендуется)
Pydantic — лучший выбор для валидации:
from pydantic import BaseModel, EmailStr, Field, validator, conint
from typing import Optional
from datetime import datetime
class UserCreate(BaseModel):
# Основные типы с автоматической валидацией
email: EmailStr # Проверяет что это валидный email
username: str = Field(..., min_length=3, max_length=20, regex=r'^[a-zA-Z0-9_-]+$')
age: conint(ge=0, le=150) # Число от 0 до 150
password: str = Field(..., min_length=8)
phone: Optional[str] = None
is_premium: bool = False
@validator('password')
def password_strong(cls, v):
"""Кастомная валидация пароля"""
if not any(char.isupper() for char in v):
raise ValueError('Пароль должен содержать заглавную букву')
if not any(char.isdigit() for char in v):
raise ValueError('Пароль должен содержать цифру')
return v
@validator('username')
def username_not_reserved(cls, v):
"""Запретить зарезервированные имена"""
reserved = ['admin', 'root', 'system', 'api']
if v.lower() in reserved:
raise ValueError(f'Username {v} зарезервирован')
return v
# Использование в FastAPI (автоматически)
from fastapi import FastAPI
app = FastAPI()
@app.post('/register')
async def register(user: UserCreate):
# user автоматически валидирован и типизирован
# Если валидация не пройдёт, FastAPI вернёт 422 с ошибками
return {"email": user.email, "username": user.username}
# Ручное использование
try:
user_data = UserCreate(
email="user@example.com",
username="john_doe",
age=25,
password="SecurePass123",
phone="+79991234567"
)
print(f"Пользователь создан: {user_data}")
except ValueError as e:
print(f"Ошибка валидации: {e}")
Способ 4: Flask с WTForms
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, IntegerField
from wtforms.validators import DataRequired, Length, Email, NumberRange, EqualTo
class RegistrationForm(FlaskForm):
email = StringField('Email', validators=[
DataRequired(),
Email(message='Некорректный email')
])
username = StringField('Username', validators=[
DataRequired(),
Length(min=3, max=20)
])
age = IntegerField('Age', validators=[
DataRequired(),
NumberRange(min=0, max=150)
])
password = PasswordField('Password', validators=[
DataRequired(),
Length(min=8)
])
confirm = PasswordField('Confirm Password', validators=[
DataRequired(),
EqualTo('password')
])
@app.route('/register', methods=['POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
# Данные валидированы
user = create_user(form.email.data, form.username.data)
return redirect('/success')
else:
# form.errors содержит ошибки
return render_template('register.html', form=form)
Способ 5: Санитизация (дополнение к валидации)
import bleach
from html import escape
def sanitize_user_input(text: str) -> str:
"""Удалить опасный HTML и скрипты"""
# Способ 1: удалить HTML теги полностью
cleaned = bleach.clean(text, tags=[], strip=True)
return cleaned
def sanitize_html_safe(text: str) -> str:
"""Оставить безопасный HTML"""
allowed_tags = ['b', 'i', 'u', 'em', 'strong', 'a', 'p', 'br']
allowed_attrs = {'a': ['href']}
return bleach.clean(text, tags=allowed_tags, attributes=allowed_attrs)
# Примеры
input_text = "<script>alert('XSS')</script>Hello"
sanitized = sanitize_user_input(input_text)
print(sanitized) # "Hello"
# Для SQL: используй параметризованные запросы (никогда f-string!)
from sqlalchemy import text
# ПРАВИЛЬНО
user = session.execute(
text("SELECT * FROM users WHERE email = :email"),
{"email": user_email}
).first()
# НЕПРАВИЛЬНО — SQL injection!
user = session.execute(
f"SELECT * FROM users WHERE email = '{user_email}'"
).first()
Способ 6: Валидация файлов
import os
from pathlib import Path
def validate_file_upload(file, max_size_mb: int = 10, allowed_extensions: list = None):
"""Валидация загруженного файла"""
if allowed_extensions is None:
allowed_extensions = ['jpg', 'png', 'pdf']
# Проверка расширения
file_ext = Path(file.filename).suffix.lower()
if file_ext not in [f'.{ext}' for ext in allowed_extensions]:
raise ValueError(f'Расширение {file_ext} не допускается')
# Проверка размера
file.seek(0, os.SEEK_END)
file_size = file.tell()
if file_size > max_size_mb * 1024 * 1024:
raise ValueError(f'Файл слишком большой (макс {max_size_mb}MB)')
# Проверка магических чисел (реальный тип файла)
file.seek(0)
file_header = file.read(8)
magic_numbers = {
b'\xFF\xD8\xFF': 'jpg',
b'\x89PNG': 'png',
b'%PDF': 'pdf'
}
detected_type = None
for magic, ftype in magic_numbers.items():
if file_header.startswith(magic):
detected_type = ftype
break
if detected_type not in allowed_extensions:
raise ValueError('Файл имеет другой тип, чем указанное расширение')
file.seek(0)
return True
Способ 7: JSON Schema валидация
from jsonschema import validate, ValidationError
schema = {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1, "maxLength": 100},
"age": {"type": "integer", "minimum": 0, "maximum": 150},
"email": {"type": "string", "format": "email"},
"tags": {
"type": "array",
"items": {"type": "string"},
"maxItems": 10
}
},
"required": ["name", "email"]
}
data = {
"name": "John",
"age": 30,
"email": "john@example.com",
"tags": ["python", "web"]
}
try:
validate(instance=data, schema=schema)
print("Данные валидны")
except ValidationError as e:
print(f"Ошибка: {e.message}")
Чеклист валидации
- Тип данных — число, строка, boolean
- Длина — минимум/максимум символов или элементов
- Формат — email, URL, phone, дата
- Диапазон — от/до для чисел
- Обязательность — required или optional
- Кастомные правила — по бизнес-логике
- Санитизация — удалить опасный код
- Rate limiting — защита от brute-force
- HTTPS — передача данных в шифрованном виде
Итог
Используй Pydantic (FastAPI) — это лучший выбор для современных Python приложений. Она обеспечивает:
- Автоматическую валидацию
- Кастомные валидаторы
- Отличные сообщения об ошибках
- Типизацию
- Документацию API (OpenAPI)
Никогда не доверяй пользовательским данным. Валидируй ВСЁ на сервере.