A small Flask/SQLite login system that demonstrates a classic SQL injection vulnerability side by side with the parameterized-query fix, so you can see exactly why one fails and the other doesn't — using the same payload against both.
The insecure implementation builds its SQL query by directly interpolating user input into the query string:
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"Because the input becomes part of the SQL statement itself, an attacker can inject SQL syntax instead of a normal value. Submitting:
Username: ' OR '1'='1' --
Password: (anything)
produces the query:
SELECT * FROM users WHERE username = '' OR '1'='1' -- ' AND password = 'anything'
'1'='1' is always true, and -- comments out the rest of the query
(including the password check), so the query returns the first user in the
table — logging the attacker in without valid credentials.
The secure implementation uses a parameterized (prepared) query:
query = "SELECT * FROM users WHERE username = ? AND password = ?"
cursor.execute(query, (username, password))
The ? placeholders keep the SQL structure fixed. The database compiles the
query first, then binds the input as literal data — never as part of the SQL
syntax. The exact same injection payload is treated as a plain string to
match against username, so the login simply fails.
sql-injection-demo/
├── main.py # Flask app with a mode toggle (Insecure / Secure)
├── insecure.py # Vulnerable login function
├── secure.py # Fixed login function using parameterized queries
├── userdatabase.py # Creates users.db with a seeded test account
├── templates/
│ ├── login.html
│ └── home.html
pip install -r requirements.txt
python userdatabase.py # one-time setup, creates users.db
python main.pyOpen http://127.0.0.1:5000, seeded test account is admin / SuperSecret123.
Try logging in with the payload above in each mode:
- Insecure Mode → logs you in without the correct password
- Secure Mode → correctly rejects it
SQL injection has been on the OWASP Top 10 for over a decade and remains one of the most common ways web applications get compromised. The underlying lesson generalizes beyond SQL: never build a command by concatenating untrusted input into it. Parameterized queries, prepared statements, and proper input handling exist in essentially every language/database combination for exactly this reason.