Skip to main content

Overview

Input validation is your first line of defense against many security vulnerabilities. This guide shows how to properly validate and sanitize user input based on the secure implementation.

Why Input Validation Matters

Prevent SQL Injection

Validate input before database queries to prevent malicious SQL execution.

Prevent XSS

Sanitize input to prevent malicious scripts from being stored or executed.

Data Integrity

Ensure data meets expected format and business rules.

Prevent DoS

Limit input size to prevent resource exhaustion attacks.

Server-Side Validation

Never rely on client-side validation alone. Always validate on the server.

Basic Input Validation

Validate all user input before processing:
secure/app.py
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username', '').strip()
        password = request.form.get('password', '')
        
        # Check for required fields
        if not username or not password:
            flash('Usuario y contraseña son requeridos', 'danger')
            return render_template('login.html')
        
        # Validate length to prevent DoS
        if len(username) > 50:
            flash('Usuario inválido', 'danger')
            return render_template('login.html')
1

Extract and sanitize input

Get input from request and strip whitespace:
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
email = request.form.get('email', '').strip()
2

Check required fields

Verify all required data is present:
if not username or not password or not email:
    flash('Todos los campos son requeridos', 'danger')
    return render_template('register.html')
3

Validate format and length

Ensure input meets expected criteria:
if len(username) < 3 or len(username) > 50:
    flash('El usuario debe tener entre 3 y 50 caracteres', 'danger')
    return render_template('register.html')

if len(password) < 6:
    flash('La contraseña debe tener al menos 6 caracteres', 'danger')
    return render_template('register.html')

Registration Validation Example

Comprehensive validation during user registration:
secure/app.py
@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form.get('username', '').strip()
        password = request.form.get('password', '')
        email = request.form.get('email', '').strip()
        
        # Required field validation
        if not username or not password or not email:
            flash('Todos los campos son requeridos', 'danger')
            return render_template('register.html')
        
        # Length validation
        if len(username) < 3 or len(username) > 50:
            flash('El usuario debe tener entre 3 y 50 caracteres', 'danger')
            return render_template('register.html')
        
        # Password strength validation
        if len(password) < 6:
            flash('La contraseña debe tener al menos 6 caracteres', 'danger')
            return render_template('register.html')

Type Validation

Validate data types to prevent type confusion attacks:
secure/app.py
@app.route('/profile')
def profile():
    if 'user_id' not in session:
        return redirect('/login')
    
    requested_id = request.args.get('id', session['user_id'])
    
    # Type validation: ensure ID is an integer
    try:
        requested_id = int(requested_id)
    except ValueError:
        flash('ID inválido', 'danger')
        return redirect('/dashboard')
Always validate and convert user input to expected types before using them in queries or business logic.

SQL Injection Prevention

Use Parameterized Queries

Always use parameterized queries (prepared statements) instead of string concatenation:

Secure

# SECURE: Parameterized query
query = "SELECT * FROM users WHERE username = %s"
cursor.execute(query, (username,))

Insecure

# INSECURE: String concatenation
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)

Prepared Statements in Action

secure/app.py
# Login query with parameterized statement
query = "SELECT * FROM users WHERE username = %s"
cursor.execute(query, (username,))
user = cursor.fetchone()

# Registration query with parameterized statement
query = "INSERT INTO users (username, password, email) VALUES (%s, %s, %s)"
cursor.execute(query, (username, hashed_password, email))

# Profile query with parameterized statement
query = "SELECT id, username, email, role, created_at FROM users WHERE id = %s"
cursor.execute(query, (requested_id,))
user = cursor.fetchone()
Parameterized queries prevent SQL injection by treating user input as data, not executable SQL code.

Input Sanitization Checklist

1

Strip whitespace

Remove leading/trailing spaces from string inputs:
username = request.form.get('username', '').strip()
2

Validate presence

Check that required fields are not empty:
if not username or not password:
    return error_response('Required fields missing')
3

Validate length

Enforce minimum and maximum lengths:
if len(username) < 3 or len(username) > 50:
    return error_response('Invalid length')
4

Validate type

Convert and validate data types:
try:
    user_id = int(request.args.get('id'))
except ValueError:
    return error_response('Invalid type')
5

Validate format

Check format using regex for emails, phone numbers, etc.:
import re
if not re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', email):
    return error_response('Invalid email format')

Client-Side Validation

While server-side validation is mandatory, client-side validation improves user experience:
secure/templates/login.html
<form method="POST">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    
    <div class="form-group">
        <label>Usuario:</label>
        <input type="text" name="username" required maxlength="50">
    </div>
    <div class="form-group">
        <label>Contraseña:</label>
        <input type="password" name="password" required>
    </div>
    <button type="submit">Entrar</button>
</form>
HTML5 attributes like required and maxlength provide basic client-side validation, but attackers can bypass these easily. Always validate server-side.

Whitelist vs Blacklist

Whitelist Approach

Recommended: Define what is allowed
# Only allow alphanumeric and underscore
if not re.match(r'^[a-zA-Z0-9_]+$', username):
    return error('Invalid characters')

Blacklist Approach

Not Recommended: Define what is blocked
# Trying to block dangerous characters
if any(c in username for c in ["'", '"', ';']):
    return error('Invalid characters')
Whitelisting is more secure because you explicitly define acceptable input, making it harder for attackers to find loopholes.

Error Handling

Handle validation errors gracefully without exposing sensitive information:
secure/app.py
try:
    query = "INSERT INTO users (username, password, email) VALUES (%s, %s, %s)"
    cursor.execute(query, (username, hashed_password, email))
    connection.commit()
    flash('Usuario registrado exitosamente!', 'success')
except sqlite3.IntegrityError:
    flash('El usuario ya existe', 'danger')
except Exception as e:
    flash('Error al registrar usuario', 'danger')
    print(f"Error: {e}")  # Log detailed error server-side only
Log detailed error information server-side, but show generic error messages to users to avoid information disclosure.

Next Steps

Output Encoding

Learn how to safely display user input to prevent XSS

CSRF Protection

Protect your forms from Cross-Site Request Forgery

Build docs developers (and LLMs) love