VOOZH about

URL: https://dev.to/bttnlight/a-flask-vulnerability-walkthrough-4d8k

⇱ A Flask Vulnerability Walkthrough - DEV Community


Machine Problem 3
Group Members: Deen, Ligero, Torres

Web applications, even simple ones, can carry serious security flaws that are easy to miss during development. In this article, I'll walk through five vulnerabilities I identified and patched in a small Flask/SQLite app featuring a login page and a user posts feed. The fixes are straightforward, but the impact of leaving them unaddressed can be severe.

Stack: Python, Flask, SQLite3
Vulnerabilities covered: SQL Injection, Cross-Site Request Forgery (CSRF), Cross-Site Scripting (XSS), Insecure Cookie Attributes

Finding 1: SQL Injection — Login Bypass

Severity: Critical
Affected file: app.pylogin() POST handler

The Problem

The login query was built by directly concatenating raw form input into a SQL string:

res = cur.execute("SELECT id FROM users WHERE username = '"
 + request.form["username"]
 + "' AND password = '"
 + request.form["password"] + "'")

This means an attacker can inject SQL syntax into the username field to manipulate the query entirely. Entering the following bypasses authentication without a valid password:

Username: ' OR '1'='1' --
Password: anything

The resulting query becomes:

SELECT id FROM users WHERE username = '' OR '1'='1' --' AND password = '...'

The -- comments out the password check, and '1'='1' is always true — so the query returns the first user in the database.

The Fix

Replace string concatenation with parameterised queries. The ? placeholder lets the database driver handle escaping safely:

cur.execute(
 "SELECT id FROM users WHERE username = ? AND password = ?",
 (request.form["username"], request.form["password"])
)

User input can no longer alter the SQL structure, no matter what it contains.

Finding 2: SQL Injection — Session Token Queries

Severity: High
Affected file: app.pylogin(), home(), posts(), logout() handlers

The Problem

Every route that authenticates the user reads a session token from a cookie and concatenates it directly into SQL. Since cookies can be freely modified by the client, a crafted value like:

' OR '1'='1

...could manipulate those queries — for example, turning a targeted DELETE in logout() into one that wipes every session in the database.

The Fix

Same as Finding 1 — parameterised queries everywhere the cookie value touches SQL:

res = cur.execute(
 "SELECT users.id, username FROM users INNER JOIN sessions ON "
 "users.id = sessions.user WHERE sessions.token = ?;",
 (request.cookies.get("session_token"),)
)

Finding 3: Cross-Site Request Forgery (CSRF)

Severity: High
Affected files: app.pyposts() handler; home.html, login.html

The Problem

Neither the /login nor the /posts endpoint verified that form submissions came from the app itself. A malicious site can host a hidden form that POSTs to the app — and since the browser automatically attaches the session cookie, the server has no way to tell the request wasn't intentional.

<!-- Hosted on attacker's site -->
<form method="POST" action="http://localhost:5000/posts" id="f">
 <input type="hidden" name="message" value="CSRF attack!">
</form>
<script>document.getElementById('f').submit();</script>

Any logged-in user who visits that page gets a post created on their account silently.

The Fix

Implement the Synchronizer Token Pattern — a server-generated secret token embedded in every form and validated on every POST. One important prerequisite: Flask's session requires a SECRET_KEY to be set, or it cannot sign session data — meaning the CSRF fix would silently fail without adding this to app.py:

app.secret_key = secrets.token_hex(32)

Then the token helpers:

def get_csrf_token():
 if "csrf_token" not in session:
 session["csrf_token"] = secrets.token_hex(32)
 return session["csrf_token"]

def validate_csrf(token):
 return token and token == session.get("csrf_token")

Add the hidden field to every form in the templates:

<input type="hidden" name="csrf_token" value="{{ csrf_token }}">

Requests that arrive without a matching token are rejected. Since the token lives in the server-side session, an attacker's site has no way to obtain it.

Finding 4: Stored Cross-Site Scripting (XSS)

Severity: High
Affected file: templates/home.html

The Problem

Posts were rendered using Jinja2's | safe filter:

<li>{{ post[0] | safe }}</li>

This explicitly disables HTML escaping, so any HTML or JavaScript stored in the database gets executed directly in the browser. Submitting this as a post:

<script>alert("XSS: " + document.cookie)</script>

...causes the script to run for every user who loads the home page, potentially leaking their session cookie to an attacker.

The Fix

Remove the | safe filter:

<li>{{ post[0] }}</li>

Jinja2 escapes HTML by default. Characters like <, >, and " become their entity equivalents (&lt;, &gt;, &quot;), so stored scripts display as plain text instead of executing.

Finding 5: Insecure Session Cookie Attributes

Severity: Medium
Affected file: app.pylogin() POST handler

The Problem

The session cookie was set without HttpOnly or SameSite attributes. Without HttpOnly, JavaScript can read the cookie — which means the XSS above could steal the session token directly via document.cookie. Without SameSite, the cookie is sent freely on cross-site requests, weakening CSRF defenses.

The Fix

Set both attributes when creating the cookie:

response.set_cookie(
 "session_token", token,
 httponly=True, # JS cannot read the cookie
 samesite="Lax" # Not sent on cross-site POSTs
)

HttpOnly cuts off the cookie-theft XSS vector entirely. SameSite=Lax adds a browser-level CSRF layer on top of the token check.