Flask Security Checklist for Heroku

This checklist covers Flask-specific security configurations, Python best practices, and Heroku deployment security for Flask applications. Note: Flask is a microframework - you must add most security features yourself.

Expedited WAF for Flask

Expedited WAF adds network level request filtering, preventing malicious attacks from ever touching your application. This enables true defense in depth for your Flask app—even if your framework has a vulnerability or is improperly configured, Expedited WAF dynamically blocks threats before they reach your code.

From experience, we’ve seen many applications where attacks technically didn’t succeed, but burned through so many server resources that the application went down anyway. By filtering malicious requests at the network edge, Expedited WAF protects both your security and your availability.

Expedited WAF protects your Flask app on Heroku

New Security Features for Your Flask App

These capabilities aren’t available out of the box with Flask, but Expedited WAF adds them instantly:

Flask Core Security

  • Configure SECRET_KEY securely

    • Store in Heroku config: heroku config:set SECRET_KEY='your-secret-key'
    • Load from environment:
    import os
    app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY')
    
    • Use cryptographically strong random string (50+ characters)
    • Never hardcode or commit to Git
  • Disable DEBUG mode in production

    app.config['DEBUG'] = os.environ.get('DEBUG', 'False') == 'True'
    
    • Verify with heroku config:get DEBUG
    • Never deploy with debug=True
  • Configure secure session cookies

    app.config['SESSION_COOKIE_SECURE'] = True  # HTTPS only
    app.config['SESSION_COOKIE_HTTPONLY'] = True  # No JavaScript access
    app.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
    app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=24)
    
  • Implement CSRF protection

    • Install Flask-WTF: pip install Flask-WTF
    from flask_wtf.csrf import CSRFProtect
    csrf = CSRFProtect(app)
    app.config['WTF_CSRF_ENABLED'] = True
    app.config['WTF_CSRF_TIME_LIMIT'] = None  # Or set specific timeout
    
    • Use {{ form.csrf_token }} in all forms
  • Add security headers manually

    • Install Flask-Talisman: pip install flask-talisman
    from flask_talisman import Talisman
    Talisman(app,
        force_https=True,
        strict_transport_security=True,
        content_security_policy={
            'default-src': "'self'",
            'script-src': "'self'",
            'style-src': "'self'"
        }
    )
    
  • Implement HTTPS/SSL enforcement

    from flask import request
    
    @app.before_request
    def before_request():
        if request.url.startswith('http://'):
            url = request.url.replace('http://', 'https://', 1)
            return redirect(url, code=301)
    
    • Or use Flask-Talisman (recommended)

Input Validation & Output Encoding

  • Validate all user input

    • Use Flask-WTF forms with validators
    • Never trust user input
    from wtforms import StringField
    from wtforms.validators import DataRequired, Length, Email
    
    class MyForm(FlaskForm):
        email = StringField('Email', validators=[DataRequired(), Email()])
    
  • Escape output to prevent XSS

    • Jinja2 auto-escapes by default - keep it enabled
    • Never use | safe unless absolutely necessary
    • Sanitize user-generated HTML with Bleach:
    import bleach
    clean_html = bleach.clean(user_input)
    
  • Prevent template injection

    • Never render user input directly as templates
    • Avoid render_template_string() with user data

Database Security

  • Use parameterized queries

    • With SQLAlchemy (recommended):
    from flask_sqlalchemy import SQLAlchemy
    db = SQLAlchemy(app)
    # Always use ORM or parameterized queries
    User.query.filter_by(email=user_email).first()
    
    • Never string concatenation for SQL
  • Configure Heroku Postgres with SSL

    import os
    from urllib.parse import urlparse
    
    database_url = os.environ.get('DATABASE_URL')
    if database_url and database_url.startswith('postgres://'):
        database_url = database_url.replace('postgres://', 'postgresql://', 1)
    
    app.config['SQLALCHEMY_DATABASE_URI'] = database_url
    app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
        'connect_args': {
            'sslmode': 'require'
        }
    }
    
  • Don’t log SQL queries in production

    app.config['SQLALCHEMY_ECHO'] = False
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    

Authentication & Authorization

  • Use established authentication libraries

    • Flask-Login for session-based auth
    • Flask-JWT-Extended for API tokens
    • Never roll your own
  • Implement Flask-Login properly

    from flask_login import LoginManager, login_required
    
    login_manager = LoginManager(app)
    login_manager.login_view = 'login'
    login_manager.session_protection = 'strong'
    
  • Hash passwords securely

    • Use Flask-Bcrypt or Werkzeug:
    from werkzeug.security import generate_password_hash, check_password_hash
    
    hashed = generate_password_hash(password, method='pbkdf2:sha256')
    check_password_hash(hashed, password)
    
  • Implement rate limiting

    • Install Flask-Limiter: pip install Flask-Limiter
    from flask_limiter import Limiter
    limiter = Limiter(app, key_func=lambda: request.remote_addr)
    
    @app.route('/login', methods=['POST'])
    @limiter.limit("5 per minute")
    def login():
        pass
    
  • Protect sensitive routes

    from flask_login import login_required
    
    @app.route('/admin')
    @login_required
    def admin():
        pass
    

File Uploads & Static Files

  • Validate file uploads

    ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'}
    
    def allowed_file(filename):
        return '.' in filename and \
               filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
    
    • Check file extension and MIME type
    • Limit file size
    • Generate unique filenames
  • Store uploads externally

    • Never store on Heroku’s ephemeral filesystem
    • Use S3 with Flask-S3 or boto3
    • Set appropriate permissions
  • Serve static files securely

    • Use WhiteNoise for production
    • Configure proper cache headers
    • Don’t serve from user-writable directories

CORS & API Security

  • Configure CORS properly

    • Install Flask-CORS: pip install Flask-CORS
    from flask_cors import CORS
    
    CORS(app, resources={
        r"/api/*": {
            "origins": ["https://yourdomain.com"],
            "methods": ["GET", "POST"],
            "allow_headers": ["Content-Type"]
        }
    })
    
    • Never use CORS(app) without restrictions in production
  • Implement API authentication

    • Use Flask-JWT-Extended for tokens
    • Implement proper token expiration
    • Validate all API inputs
  • Add API rate limiting

    • Apply to all endpoints
    • Different limits for authenticated vs anonymous

Python Dependencies

  • Pin all dependency versions

    pip freeze > requirements.txt
    
    • Use exact versions in production
    • Review dependencies regularly
  • Scan for vulnerabilities

    pip install pip-audit
    pip-audit
    
    • Run in CI/CD pipeline
    • Keep dependencies updated
  • Remove unused dependencies

    • Minimize attack surface
    • Use lightweight alternatives when possible

Environment Variables & Configuration

  • Use environment variables for all config

    class Config:
        SECRET_KEY = os.environ.get('SECRET_KEY')
        DATABASE_URL = os.environ.get('DATABASE_URL')
        MAIL_SERVER = os.environ.get('MAIL_SERVER')
    
    app.config.from_object(Config)
    
  • Store secrets in Heroku config vars

    heroku config:set SECRET_KEY='...'
    heroku config:set DATABASE_URL='...'
    
  • Never commit secrets to Git

    • Use .gitignore for .env files
    • Use python-decouple for local development

Logging & Error Handling

  • Configure production logging

    import logging
    from logging.handlers import RotatingFileHandler
    
    if not app.debug:
        handler = logging.StreamHandler()
        handler.setLevel(logging.INFO)
        app.logger.addHandler(handler)
    
  • Implement custom error pages

    @app.errorhandler(404)
    def not_found(error):
        return render_template('404.html'), 404
    
    @app.errorhandler(500)
    def internal_error(error):
        db.session.rollback()  # If using database
        return render_template('500.html'), 500
    
  • Never expose stack traces

    • Custom error pages for all errors
    • Log errors server-side only
  • Don’t log sensitive data

    • Passwords, tokens, API keys
    • Personal information
    • Credit card data

Heroku Deployment

  • Use production WSGI server

    # Procfile
    web: gunicorn app:app --log-file -
    
    • Install Gunicorn: pip install gunicorn
    • Configure workers: --workers=4
    • Enable access logs
  • Specify Python runtime

    # runtime.txt
    python-3.11.7
    
    • Use supported versions only
    • Keep runtime updated

Testing & Deployment

  • Test on staging environment

    heroku create myapp-staging
    git push staging main
    
    • Test all security configurations
    • Verify HTTPS enforcement
    • Test authentication flows
  • Write security tests

    • Test authentication/authorization
    • Test CSRF protection
    • Test input validation
  • Review before deployment

    • Check all config vars are set
    • Verify DEBUG=False
    • Test all security headers
    • Review dependencies

Flask Extensions for Security

Essential Security Extensions:

  • Flask-WTF - CSRF protection and forms
  • Flask-Talisman - Security headers and HTTPS
  • Flask-Login - User session management
  • Flask-Bcrypt - Password hashing
  • Flask-Limiter - Rate limiting
  • Flask-CORS - CORS configuration
  • Flask-SeaSurf - Additional CSRF protection

Additional Resources