PyFu

Flask Blueprints

Python Web Development Frameworks

Blueprints allow you to group related views, templates, static files, and other resources. Think of each blueprint as a mini-application.

Why Use Blueprints?

  • Separation of concerns
  • Reusability
  • Easier testing
  • Clean project structure

Blueprint Object The Blueprint in Flask is a class that lets you create a self-contained set of routes and views which can be registered on a main Flask app later.

This allows modular design in medium to large Flask applications.

flask.Blueprint(name, import_name, **kwargs)

Parameters:

  • name: The name of the blueprint (used for routing and identifying).
  • import_name: Usually __name__. Tells Flask where to look for resources like templates.
  • url_prefix (optional): A prefix added to all routes in the blueprint.
  • Other options: template_folder, static_folder, etc.

Project Structure

myapp/
│
├── app/
│   ├── __init__.py
│   ├── routes/
│   │   ├── __init__.py
│   │   ├── auth.py
│   │   └── blog.py
│   └── templates/
│
└── run.py

auth.py

from flask import Blueprint

auth_bp = Blueprint('auth', __name__, url_prefix='/auth')

@auth_bp.route('/login')
def login():
    return "Login Page"

@auth_bp.route('/logout')
def logout():
    return "Logout Page"

In this example, a blueprint named auth is created to handle authentication-related routes. The blueprint is assigned a URL prefix /auth, meaning that all routes registered under this blueprint will be accessible under that path.

The /login and /logout routes are then registered within the blueprint using the @auth_bp.route() decorator.

Once registered to the main Flask application, these routes will be accessible at /auth/login and /auth/logout.

blog.py

from flask import Blueprint

blog_bp = Blueprint('blog', __name__, url_prefix='/blog')

@blog_bp.route('/')
def index():
    return "Blog Home"

In this case, a blueprint named blog is created, which will handle routes related to the blog section of the application.

The url_prefix='/blog' ensures that all routes defined under this blueprint will be automatically prefixed with /blog when registered with the main Flask application.

Register blueprints using app/__init__.py

from flask import Flask
from .routes.auth import auth_bp
from .routes.blog import blog_bp

def init_app():
    app = Flask(__name__)
    app.register_blueprint(auth_bp)
    app.register_blueprint(blog_bp)
    return app

This code demonstrates how blueprints are registered in a modular Flask application using an application factory pattern.

This allows the app to be initialized cleanly, with all blueprints attached during startup.

run.py

from app import init_app

app = init_app()

if __name__ == '__main__':
    app.run(debug=True)

This code defines the main entry point for running the Flask application; It imports the init_app() function which we created before to initiate the application and register the blueprints.

From security perspective, Understanding how Python Blueprints work within a Flask application will give you a better insight into the application’s structure, route organization, and request handling which is highly valuable from a security perspective.

By analyzing how different parts of the application are registered, separated, and exposed through Blueprints, you can more effectively map the attack surface, identify sensitive endpoints, spot potential misconfigurations, and better assess the application’s overall security posture.

Why Flask Blueprints matter from an offensive security perspective

When I audit a blueprint-based Flask app, the first thing I look at is where access control actually lives. Blueprints frequently carry their own before_request guard, and that guard only protects the blueprint it is attached to. The moment a developer registers a route or a whole blueprint without wiring the same guard, that surface is reachable with no checks at all. The modular structure that makes blueprints clean to write is exactly what fragments authorization across the codebase, and fragmented authorization is where I find the gaps.

Here is what I look for:

  • Per-blueprint before_request guards. An auth check registered with auth_bp.before_request protects only auth_bp. Any other blueprint without an equivalent guard is open, so I enumerate every register_blueprint call and confirm each one has matching coverage.
  • Blueprints registered without the guard. A new blueprint added later, often for an admin panel or an internal API, that nobody remembered to protect. These are reachable directly even when the rest of the app looks locked down.
  • url_prefix treated as a control. A prefix like /admin is routing, not security. Nothing stops me from requesting /admin/users directly, so I never assume a prefix implies a gate.
  • Overlapping or shadowed routes. Two blueprints registering the same path, or a catch-all that quietly serves a route the developer thought lived behind a guarded blueprint.
  • Resource confusion across blueprints. Mismatched template_folder or static_folder that exposes files from one blueprint through another.

When auditing a Flask app, map every blueprint to its guard and treat any blueprint without one as unauthenticated until proven otherwise. See Broken Access Control in Flask Applications for how these gaps turn into real bypasses.

Mitigation

Do not rely on per-blueprint guards to be applied consistently by hand. Apply authentication at a single chokepoint and make the protected default explicit, then opt specific endpoints out of it. A global before_request on the application object that allow-lists public endpoints by name forces every new blueprint and every new route to be denied by default, which is the behavior you want when a developer forgets to wire a guard. Combine this with app.url_map review in CI so a newly registered blueprint cannot ship without an access decision.

PUBLIC_ENDPOINTS = {"auth.login", "auth.logout", "static"}

@app.before_request
def enforce_auth():
    # Denied by default. A new blueprint or route is protected
    # unless its endpoint is explicitly allow-listed above.
    if request.endpoint in PUBLIC_ENDPOINTS:
        return
    if not session.get("user_id"):
        abort(401)