A deliberately vulnerable banking API built with FastAPI to demonstrate race condition vulnerabilities in concurrent systems.
- 🔐 Two-factor authentication (TOTP)
- 👤 User onboarding/registration
- 💰 Cash deposits and withdrawals
- 💸 Account-to-account transfers
- 📈 Investment fund subscriptions and redemptions
- 📊 Transaction history and statements
- 🗄️ MariaDB database backend
This API is intentionally built without race condition protections to demonstrate common concurrency issues in financial systems. All database operations are vulnerable to:
- Lost updates - Concurrent modifications overwrite each other
- Dirty reads - Reading uncommitted or intermediate data
- Phantom reads - Queries return different results due to concurrent changes
- Double spending - Same money spent twice due to lack of locking
- Overdrafts - Negative balances from concurrent withdrawals
- Incorrect calculations - Fund share prices computed from stale data
DO NOT use this code in production! See comments in source code for detailed vulnerability scenarios.
.
├── config.py # Configuration and environment variables
├── database.py # SQLAlchemy models and database setup
├── auth.py # Authentication utilities (JWT, TOTP)
├── schemas.py # Pydantic request/response models
├── routes/
│ ├── __init__.py # Routes package initialization
│ ├── auth_routes.py # Onboarding, login, 2FA endpoints
│ ├── account_routes.py # Deposit, withdrawal, balance endpoints
│ ├── transfer_routes.py # Transfer and statement endpoints
│ └── fund_routes.py # Fund subscription/redemption endpoints
├── main.py # FastAPI application entry point
├── requirements.txt # Python dependencies
├── Dockerfile # Docker container definition
├── compose.yaml # Docker Compose orchestration
└── README.md # This file
- Docker 20.10+
- Docker Compose 2.0+
- Python 3.11+
- MariaDB 11+
- pip or uv package manager
-
Clone or download the project
-
Start the services
docker-compose up -d
-
View logs to get TOTP secrets for demo users
docker-compose logs api
You'll see output like:
Demo user 'alice' created - TOTP secret: JBSWY3DPEHPK3PXP Demo user 'bob' created - TOTP secret: HXDMVJECJJWSRB3H -
Access the API
- API: http://localhost:8000
- Interactive docs: http://localhost:8000/docs
- Alternative docs: http://localhost:8000/redoc
-
Stop the services
docker-compose down
-
Install dependencies
pip install -r requirements.txt
-
Set up MariaDB
# Install MariaDB (Ubuntu/Debian) sudo apt-get install mariadb-server # Start MariaDB service sudo systemctl start mariadb # Create database sudo mysql -e "CREATE DATABASE bankao;"
-
Configure environment variables
cp .env.example .env # Edit .env with your database credentials -
Run the application
python main.py # or uvicorn main:app --reload
POST /onboarding
Content-Type: application/json
{
"username": "john",
"password": "secure123"
}Response includes TOTP secret for 2FA setup.
POST /login
Content-Type: application/json
{
"username": "john",
"password": "secure123"
}POST /2fa
Content-Type: application/json
{
"username": "john",
"token": "123456" # 6-digit TOTP code
}Response includes JWT access token.
All endpoints require Authorization: Bearer <token> header.
POST /deposit
Authorization: Bearer <token>
Content-Type: application/json
{
"amount": 500.00
}POST /withdrawal
Authorization: Bearer <token>
Content-Type: application/json
{
"amount": 100.00
}GET /balance
Authorization: Bearer <token>POST /transfer
Authorization: Bearer <token>
Content-Type: application/json
{
"to_username": "alice",
"amount": 50.00
}GET /statement
Authorization: Bearer <token>POST /subscribe
Authorization: Bearer <token>
Content-Type: application/json
{
"amount": 1000.00
}POST /redemption
Authorization: Bearer <token>
Content-Type: application/json
{
"amount": 500.00
}GET /fund/infoUse the TOTP secret from logs with any authenticator app (Google Authenticator, Authy, etc.) or generate programmatically:
import pyotp
# Use secret from logs
totp = pyotp.TOTP("JBSWY3DPEHPK3PXP")
print(totp.now()) # Prints current 6-digit token# 1. First factor authentication
curl -X POST http://localhost:8000/login \
-H "Content-Type: application/json" \
-d '{"username": "alice", "password": "alice123"}'
# Get the temp_token from response
# 2. Second factor with TOTP token
curl -X POST http://localhost:8000/2fa \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TEMP_TOKEN" \
-d '{"token": "123456"}'
# Save the access_token from response# Set your token
TOKEN="your_access_token_here"
# Deposit cash
curl -X POST http://localhost:8000/deposit \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"amount": 500.00}'
# Subscribe to fund
curl -X POST http://localhost:8000/subscribe \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"amount": 200.00}'
# Transfer to another user
curl -X POST http://localhost:8000/transfer \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"to_username": "bob", "amount": 50.00}'
# Check balance
curl -X GET http://localhost:8000/balance \
-H "Authorization: Bearer $TOKEN"
# Get statement
curl -X GET http://localhost:8000/statement \
-H "Authorization: Bearer $TOKEN"The application initializes with two demo users:
| Username | Password | Initial Balance |
|---|---|---|
| alice | alice123 | $1,000.00 |
| bob | bob123 | $500.00 |
TOTP secrets are printed in console logs on first startup.
# Alice has $100, tries to send $80 to Bob twice simultaneously
# Both requests check balance ($100 >= $80) ✓
# Both requests deduct $80
# Alice ends with -$60 (overdraft!)
# Terminal 1:
curl -X POST http://localhost:8000/transfer \
-H "Authorization: Bearer $ALICE_TOKEN" \
-d '{"to_username": "bob", "amount": 80}' &
# Terminal 2 (immediately):
curl -X POST http://localhost:8000/transfer \
-H "Authorization: Bearer $ALICE_TOKEN" \
-d '{"to_username": "bob", "amount": 80}' &# Account has $100
# Deposit $50 and $30 simultaneously
# Expected: $180, Actual: $130 or $150 (lost update!)
curl -X POST http://localhost:8000/deposit \
-H "Authorization: Bearer $TOKEN" \
-d '{"amount": 50}' &
curl -X POST http://localhost:8000/deposit \
-H "Authorization: Bearer $TOKEN" \
-d '{"amount": 30}' &# Multiple subscriptions with different calculated share prices
# Can result in incorrect share allocations
for i in {1..10}; do
curl -X POST http://localhost:8000/subscribe \
-H "Authorization: Bearer $TOKEN" \
-d '{"amount": 100}' &
doneTo make this API production-ready, you would need to:
-
Use database transactions with proper isolation levels
session.begin() try: # operations session.commit() except: session.rollback()
-
Implement pessimistic locking
account = session.query(Account).with_for_update().filter_by(id=user_id).one()
-
Use optimistic locking with version numbers
class Account: version = Column(Integer, default=0) # Check version on update
-
Implement distributed locks (Redis, etc.)
with redis_lock(f"user:{user_id}"): # critical section
-
Use atomic database operations
session.execute( update(Account) .where(Account.id == user_id) .values(balance=Account.balance + amount) )
-
Implement idempotency keys for operations
-
Use message queues for sequential processing
Create a test script to trigger race conditions:
import concurrent.futures
import requests
def concurrent_transfers():
# Trigger double-spending
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
futures = [
executor.submit(transfer, 80)
for _ in range(10)
]
results = [f.result() for f in futures]
return results# Using Apache Bench
ab -n 1000 -c 50 -m POST \
-H "Authorization: Bearer $TOKEN" \
-p deposit.json \
http://localhost:8000/deposit- Never store passwords as SHA-256 hashes (use bcrypt or argon2)
- Never expose TOTP secrets in API responses
- Always use HTTPS in production
- Always implement rate limiting
- Always use proper database transactions with locking
- Always use environment variables for secrets
- Always implement proper logging and monitoring
- Always validate and sanitize all inputs
- Always implement proper error handling
- Never return detailed error messages to clients
- Framework: FastAPI 0.115.0
- Server: Uvicorn
- Database: MariaDB 11
- ORM: SQLAlchemy 2.0
- Authentication: JWT (PyJWT) + TOTP (PyOTP)
- Validation: Pydantic v2
This project is for educational purposes only. Use at your own risk.
This is an educational project demonstrating security vulnerabilities. If you'd like to contribute additional race condition examples or documentation, please:
- Keep vulnerabilities intact (this is the point!)
- Add detailed comments explaining the issues
- Document new race condition scenarios
- FastAPI Documentation
- SQLAlchemy Documentation
- Database Transaction Isolation Levels
- Race Conditions in Databases
- OWASP: Race Conditions
This is an educational project. For questions about race conditions or database concurrency, please refer to the resources above or consult database documentation.