diff --git a/.github/.keep b/.github/.keep new file mode 100644 index 0000000..e69de29 diff --git a/starter-code-simple/.env.example b/starter-code-simple/.env.example new file mode 100644 index 0000000..52c07db --- /dev/null +++ b/starter-code-simple/.env.example @@ -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 \ No newline at end of file diff --git a/starter-code-simple/.github/pull_request_template.md b/starter-code-simple/.github/pull_request_template.md new file mode 100644 index 0000000..4d81b44 --- /dev/null +++ b/starter-code-simple/.github/pull_request_template.md @@ -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 # diff --git a/starter-code-simple/.gitignore b/starter-code-simple/.gitignore new file mode 100644 index 0000000..da01337 --- /dev/null +++ b/starter-code-simple/.gitignore @@ -0,0 +1,3 @@ + +# Keep local secrets out of git +.env diff --git a/starter-code-simple/CONTRIBUTING.md b/starter-code-simple/CONTRIBUTING.md new file mode 100644 index 0000000..3d2041d --- /dev/null +++ b/starter-code-simple/CONTRIBUTING.md @@ -0,0 +1,15 @@ +# Contributing Guidelines + +## Branch Strategy +- Create feature branches from `main` using the pattern: `feat/` or `fix/`. +- 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 diff --git a/starter-code-simple/app.py b/starter-code-simple/app.py index 3d01862..4df74b7 100644 --- a/starter-code-simple/app.py +++ b/starter-code-simple/app.py @@ -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) \ No newline at end of file + debug = os.getenv("FLASK_DEBUG", "0") == "1" + app.run(host="0.0.0.0", port=int(os.getenv("PORT", "5000")), debug=debug) diff --git a/starter-code-simple/requirements.txt b/starter-code-simple/requirements.txt index 04752bd..491bfea 100644 --- a/starter-code-simple/requirements.txt +++ b/starter-code-simple/requirements.txt @@ -1,2 +1,3 @@ Flask==2.3.2 -requests==2.31.0 \ No newline at end of file +requests==2.31.0 +python-dotenv==1.0.1