A lightweight WSGI web framework for quickly building HTTP servers, APIs, and web apps — invaluable for standing up callback listeners, test targets, and tooling dashboards.
Flask is a "micro" framework: minimal core, add what you need. A few lines give you a routable HTTP server with request parsing, JSON responses, and templating. In offensive and defensive work it's the fastest way to spin up an exfil/callback listener, a deliberately vulnerable app for practice, a webhook receiver, or a small dashboard over your tooling — all on infrastructure you control.
pip install Flaskfrom flask import Flask, jsonify, request
app = Flask(__name__)
@app.route("/health")
def health():
return jsonify(status="ok")
@app.route("/callback", methods=["POST"])
def callback():
app.logger.info("callback from %s", request.remote_addr)
return jsonify(received=True), 200
if __name__ == "__main__":
# Bind to loopback only; debug must stay off
app.run(host="127.0.0.1", port=5000, debug=False)In security work Flask is most often used for small listeners: a callback receiver, a mock API, or a deliberately vulnerable target for lab exercises.
| API | Purpose |
|---|---|
Flask(__name__) |
The application object |
@app.route(rule, methods=[...]) |
Register a handler |
request.args / .form / .json |
Query, form, and JSON input |
request.headers, .remote_addr, .method |
Request metadata |
jsonify(**kwargs) |
JSON response with the right content type |
make_response(body, status, headers) |
Full control over the response |
abort(code) |
Raise an HTTP error |
render_template(name, **ctx) |
Jinja2 template with autoescaping on |
app.logger |
Application logger |
@app.errorhandler(code) |
Custom error responses |
app.run(host, port, debug) |
Development server only |
Warning
app.run() is a development server. Production requires a WSGI server such as Gunicorn or uWSGI behind a reverse proxy.
A minimal callback/collector listener (for authorized labs you control):
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/beacon", methods=["POST"])
def beacon():
data = request.get_json(silent=True) or {}
print(f"[+] check-in from {request.remote_addr}: {data}")
return jsonify(status="ok")
if __name__ == "__main__":
app.run(host="127.0.0.1", port=8000) * Running on http://127.0.0.1:8000
[+] check-in from 127.0.0.1: {'host': 'lab-agent-01', 'user': 'tester'}
127.0.0.1 - - [18/Jul/2026 10:15:02] "POST /beacon HTTP/1.1" 200 -
A small JSON API endpoint:
from flask import Flask, jsonify, request
app = Flask(__name__)
HOSTS = {"192.168.56.10": ["ssh", "http"], "192.168.56.11": ["smb"]}
@app.get("/api/hosts")
def hosts():
return jsonify(HOSTS)
@app.get("/api/host/<ip>")
def host(ip):
return jsonify(services=HOSTS.get(ip, [])), (200 if ip in HOSTS else 404)
if __name__ == "__main__":
app.run(port=8000)$ curl 127.0.0.1:8000/api/host/192.168.56.10
{"services":["ssh","http"]}
Render a simple results dashboard with a template:
from flask import Flask, render_template_string
app = Flask(__name__)
TEMPLATE = """
<h1>Scan Results</h1>
<ul>{% for h, up in results.items() %}
<li>{{ h }} — {{ 'UP' if up else 'down' }}</li>
{% endfor %}</ul>
"""
@app.get("/")
def dashboard():
results = {"192.168.56.10": True, "192.168.56.11": False}
return render_template_string(TEMPLATE, results=results)
if __name__ == "__main__":
app.run(port=8000)Scan Results
- 192.168.56.10 — UP
- 192.168.56.11 — down
- Callback / collector listeners — receive check-ins, webhooks, or exfil in authorized lab and red-team infrastructure you own.
- Vulnerable test targets — build deliberately weak endpoints to practice and demonstrate SQLi, XSS, SSRF, and IDOR safely.
- Tooling dashboards & APIs — expose scan results and controls to a team over a small internal web app.
- Webhook receivers — accept callbacks from CI, monitoring, or third-party services during automation.
- Blue-team honeytokens — serve tracking endpoints/beacon URLs to detect credential or link misuse.
Warning
Flask's built-in server is for development only. Never bind a listener to 0.0.0.0 on an untrusted network without authentication, and use gunicorn/uwsgi behind a proxy for anything long-lived.
- Leaving
debug=True— this exposes the Werkzeug interactive debugger, which allows arbitrary code execution by anyone who can reach it. - Binding to
0.0.0.0on a shared or untrusted network, publishing the listener to everyone. - Using
app.run()in production instead of a real WSGI server. - Building HTML with f-strings instead of
render_template(), losing Jinja2's autoescaping and creating XSS. - Using
render_template_string()with user input — server-side template injection leading to code execution. - Hardcoding
SECRET_KEYin source, which lets anyone forge session cookies. - Trusting
request.remote_addrbehind a proxy — it is the proxy's address unlessProxyFixis configured. - Returning raw exception text to the client, disclosing internals.
[!warning] Authorized use only Run listeners only on hosts and networks you control. Bind to
127.0.0.1unless you deliberately intend to expose the service.
debug=Trueis remote code execution. The Werkzeug debugger offers an interactive Python console on unhandled exceptions. Never enable it on any reachable interface — this is the single most damaging Flask misconfiguration.- Intentionally vulnerable lab apps must be isolated. If you build one for exercises, keep it on a host-only network with no route to production or the internet.
- Treat every request field as untrusted. Query parameters, form data, JSON bodies, headers, and cookies are all attacker-controlled.
- Keep autoescaping on. Use
render_template(); never concatenate user input into markup, and never pass it torender_template_string(). - Load
SECRET_KEYfrom the environment and generate it with [[Secrets-Module|secrets]]. - A callback listener receives attacker-influenced data during an engagement — log it, but never execute, deserialise, or interpolate it into a command.
- Add security headers and TLS at the reverse proxy for anything beyond a lab.
- Keep
debug=Trueoff outside a private lab — the debugger allows arbitrary code execution. - Validate and escape all request input; use
render_templateautoescaping, never string-concatenate HTML/SQL. - Bind to
127.0.0.1unless you deliberately need external access, and add auth when you do. - Use
jsonify(not manualstr(dict)) so responses have correct content types. - Front production deployments with a real WSGI server and TLS.
- [[FastAPI]] — async, type-hinted alternative for API-heavy tooling
- [[requests]] — the client that talks to your Flask endpoints
- [[Readme|Python for Security Professionals]] — course home