Flask Routes
Flask routes define how a Flask application responds to incoming HTTP requests and map specific URL paths to Python functions, often called view functions.
These routes control the entire behavior of the web application by determining which code is executed when a user accesses a specific endpoint.
Each route is declared using the @app.route() decorator, where the argument to the decorator specifies the URL path to be handled, and the decorated function contains the logic that generates the HTTP response for that path.
When a client sends an HTTP request to the Flask application, Flask inspects the request path and HTTP method, matches it against the registered routes, and invokes the corresponding view function.
The response returned by the view function is then sent back to the client; Flask routes can handle different HTTP methods such as GET, POST, PUT, and DELETE, and they can also accept dynamic segments within the URL path to handle parameters.
Here is a basic example demonstrating simple route definitions:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return "Index/home page"
@app.route('/about')
def about():
return "About us page"
In this example, when a client requests /, Flask calls the index() function and returns “Index/home page”.
Similarly, when /about is requested, the about() function is called and returns “About us page”.
This pattern allows Flask to build simple to highly complex web applications by mapping different URLs to specific business logic.
From a security perspective, understanding the routes and how they are mapped in a Flask application is critical for gaining insight into the application’s internal structure and identifying potential areas of risk; By analyzing the registered routes, auditors can map the attack surface, locate entry points that handle sensitive data, and identify functionality that may be exposed unintentionally.
This includes discovering administrative panels, API endpoints, file upload handlers, authentication flows, and other routes that may process user input; Proper route mapping also helps in assessing the use of authorization checks, validating whether appropriate access controls are applied to sensitive routes, and identifying any routes that lack proper authentication or input validation, potentially exposing the application to vulnerabilities such as unauthorized access, data leakage, or business logic flaws.
Why Flask Routes matter from an offensive security perspective
The route table is my map of the application. Before I touch anything else I dump every registered rule, its methods, and its converters, because the way a route is declared often leaks more attack surface than the developer realizes. A route that handles GET and POST the same way, a converter that accepts slashes, or an integer id passed straight to a lookup are all decisions made at the @app.route line, and each one is a place I push.
Here is what I look for:
- Methods that are not what they claim. A route declared without
methodsonly answersGET, but a view that branches onrequest.methodand falls through can process aPOSTbody it was never meant to. I send every method to every route and watch the responses. <path:...>converters and path traversal. Thepathconverter accepts/, so a value can climb directories when it reachesopen,send_file, oros.path.join. Any route taking a<path:...>segment that touches the filesystem is a traversal candidate. See Path Traversal in Flask Applications.- Trailing-slash and redirect behavior. Flask redirects
/adminto/admin/(or the reverse), and that 308 redirect can leak the existence of a guarded route or skip a proxy-level filter that only matched the original path. - Debug and introspection endpoints. The Werkzeug debugger,
/console, and stray developer routes left registered. These give code execution or internal detail with no auth in front of them. - IDOR through
<int:id>. A converter that validates the type but not ownership./invoice/<int:id>will happily serve another tenant’s record if the view never checks who is asking.
When auditing a Flask app, enumerate the full URL map first and treat every dynamic converter as untrusted input on its way to a sink. See Broken Access Control in Flask Applications for the authorization side of these routes.
Mitigation
Declare each route with the narrowest method set it needs, validate and constrain dynamic segments before they reach any sink, and never let a type converter stand in for an authorization or ownership check. For file-serving routes do not hand a user-controlled <path:...> value directly to open or os.path.join; resolve it against a fixed base directory and reject anything that escapes it, or use send_from_directory, which performs that containment for you. For object lookups, scope the query to the authenticated user so an <int:id> cannot reference records the caller does not own.
from flask import abort, send_from_directory
UPLOAD_DIR = "/srv/app/uploads"
@app.route("/files/<path:name>", methods=["GET"]) # explicit, GET only
def get_file(name):
# send_from_directory rejects traversal out of UPLOAD_DIR
return send_from_directory(UPLOAD_DIR, name)
@app.route("/invoice/<int:invoice_id>", methods=["GET"])
def invoice(invoice_id):
# ownership check, not just a type-valid id
record = Invoice.query.filter_by(id=invoice_id, owner_id=session["user_id"]).first()
if record is None:
abort(404)
return record.render()