DEV Community

Cover image for Day 16 — I Bypassed My Own Flask Login (And Fixed It Properly)
Hafiz Shamnad
Hafiz Shamnad

Posted on

Day 16 — I Bypassed My Own Flask Login (And Fixed It Properly)

Today wasn’t about building a new feature. It was about breaking one.

I built a small Flask authentication system with:

  • A login page
  • Session-based authentication
  • A protected dashboard
  • SQLite as the backend
  • A default admin user

It looked clean. It worked perfectly.

And it was vulnerable by design.

For the frontend UI, I used AI to generate the initial design and layout structure, then integrated it with the backend logic. It helped me move faster and focus more on the security aspect of the project.

This was a controlled demonstration of one of the most important web vulnerabilities to understand: SQL Injection (SQLi).


The Setup

The application used:

  • Flask for routing and sessions
  • SQLite for data storage
  • A simple users table with username and password fields

When a user submitted the login form, the backend constructed an SQL query dynamically using the values provided in the form.

The goal was simple: verify that the username and password exist in the database.

The problem was not in Flask.
Not in SQLite.
Not in sessions.

The problem was in how the query was constructed.


Where It Went Wrong

The login logic embedded user input directly into the SQL statement.

That means whatever the user typed into the form became part of the SQL query itself.

This is where SQL injection lives.

When input is inserted directly into an SQL string:

  • The database cannot distinguish between query structure and user data.
  • Malicious input can alter the logic of the query.
  • Authentication checks can be bypassed.

This is not a theoretical issue.
It is a structural flaw.


The Exploit

Instead of entering the correct password, I entered:

' OR '1'='1
Enter fullscreen mode Exit fullscreen mode

This changed the meaning of the SQL query.

Instead of asking:

Does this username match this password?

It effectively became:

Does this username match OR is 1 equal to 1?

Since 1=1 is always true, the WHERE condition evaluated as true.

The database returned the admin user.

The application set the session.

I was redirected to the dashboard.

Authentication bypassed.

No brute force.
No password cracking.
No guessing.

Just manipulating query logic.


Why This Is Dangerous

In this demo, the impact was limited to login bypass.

In a real-world application, SQL injection can allow:

  • Dumping entire user databases
  • Extracting password hashes
  • Modifying records
  • Deleting tables
  • Escalating privileges
  • Remote code execution in some database configurations

Authentication vulnerabilities are particularly critical because they break the trust boundary of the system.

Once an attacker bypasses authentication, everything behind it is exposed.


Root Cause Analysis

The root cause was simple:

User input was concatenated into the SQL query string.

When SQL queries are built using string formatting, the database parser processes user input as executable SQL syntax.

This violates a core secure development principle:

Never mix code and user-controlled data.


The Fix

The proper solution is to use parameterized queries (prepared statements).

With parameterized queries:

  • The SQL structure is defined first.
  • User input is passed separately.
  • The database treats input strictly as data.
  • Injection payloads lose their power.

After implementing parameterized queries:

  • The same malicious payload was treated as a plain string.
  • It did not alter query logic.
  • Authentication bypass failed.
  • The system behaved correctly.

No complex security framework was required.

Just disciplined query handling.


Lessons Learned

This exercise reinforced several key lessons:

  1. Small shortcuts create major vulnerabilities.
  2. Authentication logic must be treated as high-risk code.
  3. Secure coding is about patterns, not patches.
  4. SQL injection is still relevant in 2026.
  5. Parameterized queries should be non-negotiable.

This was not about exploiting a system.

It was about understanding how easily insecure patterns creep into even simple applications.


Flask Authentication App – Code Explanation

This application implements a simple session-based login system using Flask and SQLite. It includes:

  • Database initialization
  • Login handling
  • Session-based dashboard access
  • Logout functionality

Let’s break it down step by step.


1. Application Setup

from flask import Flask, render_template, request, redirect, url_for, session
import sqlite3

app = Flask(__name__)
app.secret_key = "sldc-secret-key"
Enter fullscreen mode Exit fullscreen mode
  • Flask initializes the web application.
  • render_template renders HTML pages.
  • request handles form input.
  • redirect and url_for manage navigation.
  • session stores authenticated user data.
  • sqlite3 connects to the SQLite database.
  • secret_key is required for securely signing session cookies.

2. Database Initialization

def init_db():
    conn = sqlite3.connect("users.db")
    cursor = conn.cursor()
    cursor.execute("""
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        username TEXT,
        password TEXT
    )
    """)
    cursor.execute("SELECT COUNT(*) FROM users")
    if cursor.fetchone()[0] == 0:
        cursor.execute("INSERT INTO users (username, password) VALUES ('admin', 'admin123')")
    conn.commit()
    conn.close()

init_db()
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Creates a database file users.db if it does not exist.
  • Creates a users table with id, username, and password.
  • Checks if the table is empty.
  • Inserts a default admin user only once.

This ensures the application is usable immediately after startup.


3. Login Route (/)

@app.route("/", methods=["GET", "POST"])
def login():
    message = ""
    if request.method == "POST":
        username = request.form["username"]
        password = request.form["password"]

        conn = sqlite3.connect("users.db")
        cursor = conn.cursor()

        query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
        cursor.execute(query)
        result = cursor.fetchone()

        conn.close()

        if result:
            session["username"] = result[1]
            return redirect(url_for("dashboard"))
        else:
            message = "Invalid credentials."

    return render_template("login.html", message=message)
Enter fullscreen mode Exit fullscreen mode

How it works:

  1. When accessed via GET, it simply renders the login page.
  2. When the login form is submitted (POST):
  • The username and password are retrieved from the form.
  • A database query checks if a matching record exists.
  • If a match is found:

    • The username is stored in the session.
    • The user is redirected to /dashboard.
  • If not:

    • An error message is displayed.

The session acts as a lightweight authentication token.


4. Dashboard Route (/dashboard)

@app.route("/dashboard")
def dashboard():
    if "username" not in session:
        return redirect(url_for("login"))
    return render_template("dashboard.html", username=session["username"])
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Checks if "username" exists in the session.
  • If not, the user is redirected to the login page.
  • If yes, the dashboard page is rendered and the username is displayed.

This enforces basic access control.


5. Logout Route (/logout)

@app.route("/logout")
def logout():
    session.clear()
    return redirect(url_for("login"))
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Clears all session data.
  • Redirects the user back to login.
  • Effectively logs the user out.

6. Running the Application

if __name__ == "__main__":
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode
  • Starts the Flask development server.
  • debug=True enables automatic reload and detailed error messages during development.

Complete Authentication Flow

  1. User visits /
  2. Submits login form
  3. Backend validates credentials against database
  4. If valid, session is created
  5. User gains access to /dashboard
  6. Logout clears session

Important Architectural Observations

  • Authentication is session-based.
  • SQLite is used as a lightweight relational database.
  • Access control depends on session presence.
  • Database interaction is done synchronously per request.
  • The system is suitable for demonstration or learning environments.

Result

Login Page






Why I Built This

As part of my ongoing cybersecurity learning journey, I want to:

  • Build vulnerable systems intentionally
  • Exploit them in a controlled environment
  • Fix them properly
  • Understand the “why,” not just the “how”

Because real security knowledge comes from breaking and repairing systems yourself.

Day 16 was not about writing more code. It was about writing safer code. And that difference matters.

Top comments (0)