Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added .github/.keep
Empty file.
13 changes: 13 additions & 0 deletions starter-code-simple/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Flask app settings
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=replace_with_a_secure_random_string

# Database (if using SQLite you can leave it as-is, otherwise point to Postgres/MySQL etc.)
DATABASE_URL=sqlite:///app.db

# JWT or session tokens (dummy values)
JWT_SECRET_KEY=replace_with_another_secure_random_string

# Debug settings
DEBUG=True
19 changes: 19 additions & 0 deletions starter-code-simple/.github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Pull Request Template

## Description
- Provide a clear and concise description of the changes.

## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Documentation update
- [ ] Security fix

## Checklist
- [ ] Tests added or updated
- [ ] Documentation updated
- [ ] Code follows style guidelines
- [ ] Security checks passed

## Related Issues
- Closes #
3 changes: 3 additions & 0 deletions starter-code-simple/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

# Keep local secrets out of git
.env
15 changes: 15 additions & 0 deletions starter-code-simple/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Contributing Guidelines

## Branch Strategy
- Create feature branches from `main` using the pattern: `feat/<short-name>` or `fix/<short-name>`.
- Open a Pull Request (PR) to `main` and request a review before merging.

## Setup
```bash
python -m venv .venv
source .venv/bin/activate # Linux/Mac
# OR
.\venv\Scripts\activate # Windows

pip install -r requirements.txt
cp .env.example .env # Set your environment variables
161 changes: 95 additions & 66 deletions starter-code-simple/app.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,110 @@
# Simple Python API - Starting Point for GitHub Classroom Assignment
# This code has intentional security flaws for educational purposes

from flask import Flask, request, jsonify
import os
import sqlite3
import hashlib
import logging
from flask import Flask, request, jsonify
from werkzeug.security import generate_password_hash, check_password_hash

try:
from dotenv import load_dotenv # for local dev
load_dotenv()
except Exception:
pass

app = Flask(__name__)

# Security Issue: Hardcoded secrets
DATABASE_URL = "postgresql://admin:password123@localhost/prod"
API_SECRET = "sk-live-1234567890abcdef"
DB_PATH = os.getenv("DATABASE_PATH", "users.db")

# ---------- logging (no secrets) ----------
logging.basicConfig(
level=os.getenv("LOG_LEVEL", "INFO"),
format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger(__name__)

# ---------- helpers ----------
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn

def init_db():
with get_conn() as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""
)

def validate_username(u: str) -> bool:
return isinstance(u, str) and 3 <= len(u) <= 64

def get_db_connection():
return sqlite3.connect('users.db')
def validate_password(p: str) -> bool:
return isinstance(p, str) and 8 <= len(p) <= 256

@app.route('/health')
# ---------- routes ----------
@app.route("/health", methods=["GET"])
def health_check():
return jsonify({"status": "healthy", "database": DATABASE_URL})
return jsonify({"status": "healthy", "database": DB_PATH}), 200

@app.route('/users', methods=['GET'])
@app.route("/users", methods=["GET"])
def get_users():
conn = get_db_connection()
users = conn.execute('SELECT id, username FROM users').fetchall()
conn.close()
return jsonify({"users": [{"id": u[0], "username": u[1]} for u in users]})
with get_conn() as conn:
rows = conn.execute("SELECT id, username, created_at FROM users ORDER BY id").fetchall()
# never return password hashes
return jsonify([dict(r) for r in rows]), 200

@app.route('/users', methods=['POST'])
@app.route("/users", methods=["POST"])
def create_user():
data = request.get_json()
username = data.get('username')
password = data.get('password')

# Security Issue: Weak password hashing
hashed_password = hashlib.md5(password.encode()).hexdigest()

conn = get_db_connection()
# Security Issue: SQL injection vulnerability
conn.execute(
f"INSERT INTO users (username, password) VALUES ('{username}', '{hashed_password}')"
)
conn.commit()
conn.close()

# Security Issue: Logging sensitive information
print(f"Created user: {username} with password: {password}")
return jsonify({"message": "User created", "username": username})

@app.route('/login', methods=['POST'])
data = request.get_json(silent=True) or {}
username = (data.get("username") or "").strip()
password = data.get("password") or ""

if not (validate_username(username) and validate_password(password)):
return jsonify({"error": "invalid username or password"}), 400

pwd_hash = generate_password_hash(password)

try:
with get_conn() as conn:
conn.execute(
"INSERT INTO users (username, password_hash) VALUES (?, ?)",
(username, pwd_hash)
)
log.info("created user '%s'", username)
return jsonify({"message": "user created", "username": username}), 201
except sqlite3.IntegrityError:
return jsonify({"error": "username already exists"}), 409

@app.route("/login", methods=["POST"])
def login():
data = request.get_json()
username = data.get('username')
password = data.get('password')

hashed_password = hashlib.md5(password.encode()).hexdigest()

conn = get_db_connection()
# Security Issue: SQL injection vulnerability
query = f"SELECT * FROM users WHERE username='{username}' AND password='{hashed_password}'"
user = conn.execute(query).fetchone()
conn.close()

if user:
return jsonify({"message": "Login successful", "user_id": user[0]})
return jsonify({"message": "Invalid credentials"}), 401
data = request.get_json(silent=True) or {}
username = (data.get("username") or "").strip()
password = data.get("password") or ""

def init_db():
conn = get_db_connection()
conn.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL
)
''')
conn.commit()
conn.close()
if not (validate_username(username) and isinstance(password, str)):
return jsonify({"error": "invalid credentials"}), 400

with get_conn() as conn:
row = conn.execute(
"SELECT password_hash FROM users WHERE username = ?",
(username,)
).fetchone()

if not row or not check_password_hash(row["password_hash"], password):
log.warning("failed login for '%s'", username)
return jsonify({"error": "invalid credentials"}), 401

log.info("successful login for '%s'", username)
# For exercise-just acknowledge-do NOT return secrets/tokens here
return jsonify({"message": "login ok"}), 200

if __name__ == '__main__':
# ---------- bootstrap ----------
if __name__ == "__main__":
init_db()
app.run(debug=True)
debug = os.getenv("FLASK_DEBUG", "0") == "1"
app.run(host="0.0.0.0", port=int(os.getenv("PORT", "5000")), debug=debug)
3 changes: 2 additions & 1 deletion starter-code-simple/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Flask==2.3.2
requests==2.31.0
requests==2.31.0
python-dotenv==1.0.1