PyFu

Flask Decorators

Python Web Development Frameworks

Flask decorators are powerful tools used to modify the behavior of view functions and handle common web app tasks such as routing, authorization, and error handling.

First, let’s explain what a decorator is in general; A decorator in Python is a special type of function that wraps another function, allowing you to modify or extend its behavior without changing the original function’s code.

Technically, a decorator is simply a callable that takes a function as input and returns a new function as output.

This mechanism allows developers to apply reusable behavior to multiple functions in a clean, readable, and modular way.

Python’s @decorator syntax makes applying decorators easy and intuitive.

For example, we have this decorator:

def custom_decorator(func):
    def wrapper():
        print("Before function")
        func()
        print("After function")
    return wrapper

@custom_decorator
def print_hello():
    print("Hello from print_hello method!")

print_hello()

The output of this script will be:

python3 python_decorator.py
---
Before function
Hello from print_hello method!
After function

The custom_decorator function takes another function (func) as its input. Inside it, a nested wrapper() function is defined, which adds behavior before and after calling the original function.

The decorator returns this wrapper() function, which means that when print_hello() is defined, it is effectively replaced by wrapper().

As a result, when print_hello() is called, the wrapper() function is actually executed, allowing the additional behavior to run before and after the original function’s logic.

So When you write @custom_decorator above a function definition, it is equivalent to manually calling custom_decorator and passing the function to it.

Now after we understand how decorators work, let’s discuss some Flask default decorators.

Flask Decorators

In Flask, @app.before_request and @app.after_request are two important decorators that allow developers to hook into the request lifecycle and execute custom logic automatically for every request.

app.before_request

The @app.before_request decorator registers a function that is executed before Flask routes the request to the appropriate view function.

It runs for every incoming HTTP request and is typically used for tasks such as logging, authentication checks, request validation, or setting up request-level context.

For example:

@app.before_request
def custom_request_logger():
	path = request.path
	print(f"Incoming request to {path} from {request.remote_addr}")

the function custom_request_logger() is registered with @app.before_request.

Inside this function, request.path is used to retrieve the requested URL path, and request.remote_addr extracts the client’s IP address.

The function logs this information to the console, giving the developer visibility into which endpoints are being accessed and from which IP addresses.

app.after_request

The @app.after_request decorator registers a function that is executed after the view function has processed the request and just before the response is sent back to the client.

It receives the response object as its argument and allows developers to modify the response if necessary.

For example:

@app.after_request
def add_header(response):
    response.headers["X-App-Version"] = "1.0"
    return response

In this example, the function add_header(response) adds a custom HTTP response header X-App-Version with the value 1.0.

This header will appear in every HTTP response, allowing clients or monitoring systems to see which version of the application they are interacting with.

Custom decorators Developers often write custom decorators to execute additional logic on specific routes, most commonly for authorization checks. In Flask, decorators can be applied to any route to enforce access control policies based on session data, roles, or permissions.

For example, the following code defines a decorator called admin_required, which checks whether the current user’s role stored in the session is set to admin.

If the check fails, it returns an “Access Denied” response with a 403 status code; otherwise, it allows the original view function to execute.

The decorator is then applied to the /admin route to restrict access to administrative users only.

def admin_required(f):
    def decorated_function(*args, **kwargs):
        if session.get("role") != "admin":
            return "Access Denied", 403
        return f(*args, **kwargs)
    return decorated_function

@app.route('/admin')
@require_admin
def admin():
    return "This is admin content only"

If we attempt to send an HTTP request using curl to the host http://pyfu.local:5000 where the Flask application is running, the request is processed by Flask and the custom admin_required decorator is applied to the /admin route.

For example:

» curl http://pyfu.local:5000/admin -v
* Host pyfu.local:5000 was resolved.
* IPv6: (none)
* IPv4: 127.0.0.1, 127.0.0.1
*   Trying 127.0.0.1:5000...
* Connected to pyfu.local (127.0.0.1) port 5000
> GET /admin HTTP/1.1
> Host: pyfu.local:5000
> User-Agent: curl/8.5.0
> Accept: */*
> 
< HTTP/1.1 403 FORBIDDEN
< Server: Werkzeug/3.1.3 Python/3.12.3
< Date: Sat, 31 May 2025 15:37:03 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 13
< X-App-Version: 1.0
< Connection: close
< 
* Closing connection
Access Denied%                    

Since the request does not include any session information (specifically, no valid session cookie with role=admin), the decorator logic returns a 403 Forbidden response along with the message “Access Denied”.

Auditing custom decorators and analyzing how they are applied to Flask routes is an important part of any Flask application security review. Custom decorators are often used to enforce authorization and access control logic; however, if these decorators contain logical flaws or incorrect assumptions, they may be susceptible to bypasses that allow unauthorized access to protected routes.

In some cases, developers may also unintentionally omit applying the required decorators to certain routes, accidentally exposing sensitive data or administrative functionality to unauthenticated users.

As Flask does not natively provide a mapping of which decorators are applied to which routes, security auditors need to carefully analyze both the route registration logic and the decorator implementations to identify any potential weaknesses in the application’s access control model.

Why Flask Decorators matter from an offensive security perspective

Decorators are where Flask developers put their authorization logic, and they are also where I find most of the access control bugs. A decorator is just a function that wraps another function, so every subtle mistake in how it wraps the view becomes an authorization mistake. I read the decorator body line by line and I check the order it sits in relative to @app.route, because both decide whether the check actually runs before the view does.

Here is what I look for:

  • Decorator order relative to @app.route. @app.route must sit on top and the auth decorator below it, so the auth wrapper is the one Flask actually calls. When the order is reversed, the route can register the unwrapped view and the check never runs. I read the stack top to bottom on every protected route.
  • Checks that fall through instead of returning. A custom decorator that logs or computes a verdict but never returns an early 403/401 on failure, so execution drops into f(*args, **kwargs) regardless. The check looks present but does nothing.
  • A @login_required that does not stop the view. Decorators that set a flag, raise an exception that is swallowed elsewhere, or only redirect for browsers while still serving the response body to a direct API call.
  • Missing @wraps. Without functools.wraps, the wrapper loses the original function’s __name__, which can collide endpoint names, break url_for, and hide endpoints during route mapping so a protected view is harder to spot and easier to leave unguarded.
  • Decorators applied to some routes but not others. The same sensitive action exposed through a second view that nobody decorated. Flask gives you no built-in map of this, so I diff the route list against the decorated set by hand.

When auditing a Flask app, treat every authorization decorator as guilty until I have confirmed it returns early on failure and sits below @app.route. See Broken Access Control in Flask Applications for how these decorator gaps become full bypasses, and Flask Routes for mapping which views carry no decorator at all.

Mitigation

Write authorization decorators so the failure path returns before the wrapped view can run, and always preserve identity with functools.wraps. The decorator should compute the verdict, return the denial response immediately on failure, and only call the view on the success path, with no branch that falls through. Keep @app.route as the outermost decorator and the auth decorator directly beneath it so Flask registers the wrapped callable, not the bare view. Pairing this with @wraps keeps endpoint names intact so route mapping and url_for behave and no protected view hides itself.

from functools import wraps
from flask import session, abort

def admin_required(f):
    @wraps(f)                       # preserve __name__ so the endpoint maps correctly
    def decorated(*args, **kwargs):
        if session.get("role") != "admin":
            abort(403)              # return on the failure path, never fall through
        return f(*args, **kwargs)   # reached only when the check passes
    return decorated

@app.route("/admin")               # route stays outermost
@admin_required                    # auth wrapper directly beneath it
def admin():
    return "This is admin content only"