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.py — login() 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.py — login(), 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.py — posts() 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:
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 (<, >, "), so stored scripts display as plain text instead of executing.
Finding 5: Insecure Session Cookie Attributes
Severity: Medium
Affected file: app.py — login() 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.
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.
Top comments (0)