A dual-mode banking web app built with Flask + MySQL for learning web security.
Toggle between a vulnerable app and a hardened one using the UI banner or the .env file.
- Backend: Python / Flask Framework
- Database: MySQL
- Frontend: HTML, CSS, JavaScript
Make sure you're in directory banking-app
cd banking-app1. Install dependencies
pip install -r requirements.txt2. Configure the database
Copy .env.example to .env and fill in your MySQL credentials and a secret key.
copy .env.example .env3. Initialize the database
python init_db.py4. Run the app
python app.py1. Create and activate a virtual environment
python3 -m venv venv
source venv/bin/activate2. Install dependencies
pip install -r requirements.txt3. Configure the database
Copy .env.example to .env and fill in your MySQL credentials and a secret key.
cp .env.example .env4. Initialize the database
python3 init_db.py5. Run the app
python3 app.pyOpen http://127.0.0.1:5000 in your browser.
Demo accounts
| Username | Password |
|---|---|
| alice | password123 |
| bob | espresso95 |
| charlie | raspberry67 |
| admin | chocolate45 |
Use the banner at the top of every page to switch between modes at runtime — no restart needed.
When you switch modes, the app updates immediately. If you then re-run init_db.py to reseed the database, it will pick up the current mode automatically (passwords will be bcrypt-hashed in secure mode, plaintext in insecure mode).
You can also set the initial mode before starting the app by editing SECURE_MODE in your .env file:
SECURE_MODE=false # intentionally vulnerable (default, for demos)
SECURE_MODE=true # hardened implementation
After changing .env directly, restart the app for it to take effect.
| Vulnerability | Insecure | Secure |
|---|---|---|
| SQL Injection | Raw f-string queries | Parameterized %s placeholders |
| Stored XSS | {{ description | safe }} in templates |
Jinja2 auto-escaping |
| Broken Auth | Plaintext passwords, hardcoded secret key | bcrypt, env-var secret key |
| IDOR | ?user_id= accepted from URL |
user_id always from session |
| CSRF | No token on forms | Flask-WTF CSRF protection |
A companion site at ../attacker-site/ demonstrates the CSRF vulnerability (Section 3.5 of the security report). When a victim visits it while logged in to VaultBank in insecure mode, a hidden form auto-submits a fund transfer on their behalf — without any interaction from the victim.
Run in a separate terminal from the project root:
cd attacker-site
python3 -m http.server 8000Open http://127.0.0.1:8000 while logged in to VaultBank. The page shows a fake prize lure (what the victim sees) alongside a step-by-step explanation of the attack payload and a 3-second countdown before the hidden form fires.
URL params let you adjust the demo without editing any files:
| Param | Default | Purpose |
|---|---|---|
victim_id |
1 |
Sender account (exploits IDOR to target any user) |
receiver |
charlie |
Attacker-controlled recipient |
amount |
500 |
Transfer amount |
Example: http://127.0.0.1:8000?victim_id=1&receiver=charlie&amount=500
The attack only succeeds when VaultBank is running in insecure mode. In secure mode, Flask-WTF rejects the request due to a missing CSRF token.
banking-app/
├── app.py # Flask app factory
├── config.py # Reads SECURE_MODE from .env
├── database.py # MySQL connection
├── init_db.py # DB setup and seed script
├── .env # Local config (not committed)
├── .env.example # Template for .env
├── routes/ # Auth, dashboard, transfer, history
├── security/
│ ├── queries_insecure.py # Vulnerable implementations
│ └── queries_secure.py # Hardened implementations
├── static/ # CSS and JS
└── templates/ # Jinja2 HTML templates