From 55577a8c9416236176e681b5f2a5afbdf96ab21b Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 22:34:38 +0000 Subject: [PATCH 1/4] GitHub Classroom Feedback --- .github/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/.keep diff --git a/.github/.keep b/.github/.keep new file mode 100644 index 0000000..e69de29 From 95b00890e868248527f3cd33b1570229ca757104 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Tue, 23 Sep 2025 22:34:39 +0000 Subject: [PATCH 2/4] Setting up GitHub Classroom Feedback From f32992e31e99244a5ec1bc5c6325b22bfdf0e490 Mon Sep 17 00:00:00 2001 From: Mashruf Date: Sun, 28 Sep 2025 23:43:27 -0400 Subject: [PATCH 3/4] Completed In-Class Assignment W0D2 --- .gitignore | 4 + README.md | 2 +- references.md | 8 ++ starter-code-simple/app.py | 246 +++++++++++++++++++++++++++---------- 4 files changed, 192 insertions(+), 68 deletions(-) create mode 100644 .gitignore create mode 100644 references.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff65ab5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# ------------------------- +# Database files +# ------------------------- +users.db \ No newline at end of file diff --git a/README.md b/README.md index 74c245a..7392a23 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Professional Git Workflows β€” Student Guide - +## Additional context and AI-assisted resources are documented in [references.md](./references.md) ## Overview **Format:** In-class breakout exercises + after-class individual assignment **Language:** Python diff --git a/references.md b/references.md new file mode 100644 index 0000000..c25f771 --- /dev/null +++ b/references.md @@ -0,0 +1,8 @@ +# πŸ“š References + +This project was refactored as part of a security-focused learning exercise with the assistance of **ChatGPT (OpenAI)**. +AI guidance was used to: + +- Identify and document key security vulnerabilities, including hardcoded secrets, SQL injection, and weak password hashing. +- Recommend and implement modern best practices such as `bcrypt` for secure password storage, parameterized SQL queries, and environment variable configuration. +- Improve code documentation, structure, and logging practices following industry standards. \ No newline at end of file diff --git a/starter-code-simple/app.py b/starter-code-simple/app.py index 3d01862..b3fdda7 100644 --- a/starter-code-simple/app.py +++ b/starter-code-simple/app.py @@ -1,81 +1,193 @@ -# Simple Python API - Starting Point for GitHub Classroom Assignment -# This code has intentional security flaws for educational purposes +# app.py β€” Secure Flask User Management API +# Notes: +# - Uses environment variables for secrets/config +# - Parameterized SQL everywhere (no string formatting) +# - bcrypt for password hashing (with constant-time verification) +# - Minimal health output (no infra leakage) +# - Input validation + structured error responses +# - Safe logging (no secrets, no PII like passwords) +# - Context-managed DB connections -from flask import Flask, request, jsonify +import os +import re +import json +import logging import sqlite3 -import hashlib +from typing import Tuple, Optional + +from flask import Flask, request, jsonify +import bcrypt + +# ------------------------- +# Config +# ------------------------- +APP_ENV = os.getenv("APP_ENV", "development") # development | test | production +DB_PATH = os.getenv("SQLITE_PATH", "users.db") # keep sqlite for the assignment +# Example: API_SECRET used for future features (JWT signing, etc.) +API_SECRET = os.getenv("API_SECRET", None) # DO NOT hardcode; may be None in dev +# ------------------------- +# App & Logging +# ------------------------- app = Flask(__name__) -# Security Issue: Hardcoded secrets -DATABASE_URL = "postgresql://admin:password123@localhost/prod" -API_SECRET = "sk-live-1234567890abcdef" +class JsonFormatter(logging.Formatter): + def format(self, record): + payload = { + "level": record.levelname, + "msg": record.getMessage(), + "logger": record.name, + } + if record.exc_info: + payload["exc_info"] = self.formatException(record.exc_info) + return json.dumps(payload) -def get_db_connection(): - return sqlite3.connect('users.db') +handler = logging.StreamHandler() +handler.setFormatter(JsonFormatter()) +app.logger.setLevel(logging.INFO if APP_ENV != "development" else logging.DEBUG) +app.logger.addHandler(handler) -@app.route('/health') -def health_check(): - return jsonify({"status": "healthy", "database": DATABASE_URL}) +# ------------------------- +# Helpers +# ------------------------- +def get_conn() -> sqlite3.Connection: + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn -@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]}) +USERNAME_RE = re.compile(r"^[A-Za-z0-9_]{3,32}$") -@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']) -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 +def validate_credentials(username: Optional[str], password: Optional[str]) -> Tuple[bool, str]: + if not isinstance(username, str) or not USERNAME_RE.fullmatch(username or ""): + return False, "Username must be 3–32 chars (letters, numbers, underscore)." + if not isinstance(password, str) or len(password) < 8: + return False, "Password must be at least 8 characters." + return True, "" + +def hash_password(password: str) -> str: + # bcrypt returns bytes like b'$2b$12$...'; store as utf-8 string + return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") +def verify_password(password: str, hashed: str) -> bool: + try: + return bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8")) + except Exception: + # In case legacy/invalid hashes exist, fail closed without leaking detail + return False + +def json_error(message: str, status: int = 400): + return jsonify({"error": message}), status + +# ------------------------- +# DB Init (idempotent) +# ------------------------- 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 + with get_conn() as conn: + conn.execute( + """ + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + is_admin INTEGER NOT NULL DEFAULT 0 + ) + """ ) - ''') - conn.commit() - conn.close() + conn.commit() + +# ------------------------- +# Routes +# ------------------------- +@app.route("/health", methods=["GET"]) +def health(): + # Intentionally minimal to avoid leaking config + return jsonify({"status": "healthy", "env": APP_ENV}) + +@app.route("/users", methods=["GET"]) +def list_users(): + with get_conn() as conn: + rows = conn.execute("SELECT id, username FROM users ORDER BY id ASC").fetchall() + users = [{"id": row["id"], "username": row["username"]} for row in rows] + return jsonify({"users": users}) + +@app.route("/users", methods=["POST"]) +def create_user(): + try: + data = request.get_json(force=True, silent=False) + except Exception: + return json_error("Invalid JSON body.", 400) + + username = (data or {}).get("username") + password = (data or {}).get("password") + + ok, msg = validate_credentials(username, password) + if not ok: + return json_error(msg, 400) + + pw_hash = hash_password(password) + + try: + with get_conn() as conn: + conn.execute( + "INSERT INTO users (username, password_hash) VALUES (?, ?)", + (username, pw_hash), + ) + conn.commit() + except sqlite3.IntegrityError: + return json_error("Username already exists.", 409) + + app.logger.info(f"user_created username={username}") # safe: no password + return jsonify({"message": "User created", "username": username}), 201 + +@app.route("/login", methods=["POST"]) +def login(): + try: + data = request.get_json(force=True, silent=False) + except Exception: + return json_error("Invalid JSON body.", 400) + + username = (data or {}).get("username") + password = (data or {}).get("password") + + if not isinstance(username, str) or not isinstance(password, str): + return json_error("Username and password are required.", 400) + + with get_conn() as conn: + row = conn.execute( + "SELECT id, username, password_hash FROM users WHERE username = ?", + (username,), + ).fetchone() + + if not row or not verify_password(password, row["password_hash"]): + # Don’t reveal which field failed + app.logger.info(f"login_failed username={username}") + return json_error("Invalid credentials.", 401) + + app.logger.info(f"login_success username={username} user_id={row['id']}") + # For the assignment we return a simple payload. In production, issue a JWT/session cookie. + return jsonify({"message": "Login successful", "user_id": row["id"]}) + +# ------------------------- +# Error Handling +# ------------------------- +@app.errorhandler(404) +def not_found(_): + return json_error("Route not found.", 404) + +@app.errorhandler(405) +def method_not_allowed(_): + return json_error("Method not allowed.", 405) + +@app.errorhandler(500) +def internal_error(e): + app.logger.error("internal_error", exc_info=e) + return json_error("Internal server error.", 500) -if __name__ == '__main__': +# ------------------------- +# Entrypoint +# ------------------------- +if __name__ == "__main__": init_db() - app.run(debug=True) \ No newline at end of file + # Never force debug=True; respect env + debug = APP_ENV == "development" + app.run(host="0.0.0.0", port=int(os.getenv("PORT", "5000")), debug=debug) From 23c6c65a961c8e4707b48841baa2a6a71dea79c0 Mon Sep 17 00:00:00 2001 From: Mashruf Date: Sun, 28 Sep 2025 23:54:24 -0400 Subject: [PATCH 4/4] Added comment to references.md --- references.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/references.md b/references.md index c25f771..adaead0 100644 --- a/references.md +++ b/references.md @@ -5,4 +5,6 @@ AI guidance was used to: - Identify and document key security vulnerabilities, including hardcoded secrets, SQL injection, and weak password hashing. - Recommend and implement modern best practices such as `bcrypt` for secure password storage, parameterized SQL queries, and environment variable configuration. -- Improve code documentation, structure, and logging practices following industry standards. \ No newline at end of file +- Improve code documentation, structure, and logging practices following industry standards. + +# Here is a small comment pending code review. \ No newline at end of file