Initial commit: TMT lightweight threat modeling toolkit

This commit is contained in:
Kevin Thomas committed 2026-02-07 13:54:28 -05:00
commit 717a0b819f
29 files changed
+4214

No files matched your search

+1
View File
@@ -0,0 +1 @@
"""Test fixtures for TMT scanner validation."""
+174
View File
@@ -0,0 +1,174 @@
"""Secure API fixture demonstrating proper defensive patterns.
This file contains well-secured API endpoints that should produce
minimal findings when scanned by TMT. Used to validate that scanners
do not generate excessive false positives.
"""
import secrets
from datetime import datetime, timedelta, timezone
from functools import wraps
from flask import Flask, request, jsonify, session
from flask_limiter import Limiter
from werkzeug.security import generate_password_hash, check_password_hash
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
# ──────────────────────────────────────────────────────────────────────────────
# Secure session configuration
# ──────────────────────────────────────────────────────────────────────────────
app.config["SESSION_COOKIE_SECURE"] = True
app.config["SESSION_COOKIE_HTTPONLY"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
# ──────────────────────────────────────────────────────────────────────────────
# Rate limiter setup
# ──────────────────────────────────────────────────────────────────────────────
limiter = Limiter(app=app, default_limits=["100 per hour"])
# ──────────────────────────────────────────────────────────────────────────────
# Strict CORS with explicit origin
# ──────────────────────────────────────────────────────────────────────────────
ALLOWED_ORIGINS = ["https://app.example.com"]
@app.after_request
def add_cors(response):
"""Add CORS headers with explicit origin allowlist."""
origin = request.headers.get("Origin", "")
if origin in ALLOWED_ORIGINS:
response.headers["Access-Control-Allow-Origin"] = origin
return response
# ──────────────────────────────────────────────────────────────────────────────
# Authentication decorator with login_required check
# ──────────────────────────────────────────────────────────────────────────────
def login_required(f):
"""Decorator that enforces authentication on protected routes."""
@wraps(f)
def decorated(*args, **kwargs):
"""Check session for authenticated user before proceeding."""
if "user_id" not in session:
return jsonify({"error": "Unauthorized"}), 401
return f(*args, **kwargs)
return decorated
# ──────────────────────────────────────────────────────────────────────────────
# Secure login with bcrypt-equivalent hashing and session regeneration
# ──────────────────────────────────────────────────────────────────────────────
@app.post("/api/login")
@limiter.limit("5 per minute")
def login():
"""Authenticate with rate limiting and session regeneration."""
schema = LoginSchema()
data = schema.validate(request.json)
user = db.users.find_one({"email": data["email"]})
if user and check_password_hash(user["password"], data["password"]):
session.regenerate()
session["user_id"] = str(user["_id"])
return jsonify({"status": "ok"})
return jsonify({"error": "Invalid credentials"}), 401
# ──────────────────────────────────────────────────────────────────────────────
# Secure invite with rate limit, expiry, and single-use enforcement
# ──────────────────────────────────────────────────────────────────────────────
@app.post("/api/invite")
@login_required
@limiter.limit("5 per hour")
def generate_invite():
"""Generate a time-limited, single-use invitation token."""
token = secrets.token_urlsafe(32)
expires_at = datetime.now(timezone.utc) + timedelta(hours=72)
db.invites.insert_one(
{
"token": token,
"created_by": session["user_id"],
"expires_at": expires_at,
"is_used": False,
"idempotency_key": request.headers.get("Idempotency-Key"),
}
)
return jsonify({"invite_token": token})
# ──────────────────────────────────────────────────────────────────────────────
# Atomic invite acceptance with transaction and single-use mark
# ──────────────────────────────────────────────────────────────────────────────
@app.post("/api/accept-invite")
@limiter.limit("10 per hour")
def accept_invite():
"""Accept an invitation atomically with single-use enforcement."""
schema = AcceptInviteSchema()
data = schema.validate(request.json)
with db.transaction():
invite = db.invites.find_one_and_update(
{
"token": data["token"],
"is_used": False,
"expires_at": {"$gt": datetime.now(timezone.utc)},
},
{"$set": {"is_used": True, "used_at": datetime.now(timezone.utc)}},
)
if not invite:
return jsonify({"error": "Invalid or expired invite"}), 400
db.users.insert_one({"email": data["email"], "role": "member"})
return jsonify({"status": "account created"})
# ──────────────────────────────────────────────────────────────────────────────
# Atomic balance transfer with select_for_update
# ──────────────────────────────────────────────────────────────────────────────
@app.post("/api/transfer")
@login_required
@limiter.limit("20 per hour")
def transfer():
"""Transfer balance atomically with proper locking."""
schema = TransferSchema()
data = schema.validate(request.json)
idempotency_key = request.headers.get("Idempotency-Key")
with db.transaction():
sender = db.accounts.find_one_and_update(
{"user_id": session["user_id"], "balance": {"$gte": data["amount"]}},
{"$inc": {"balance": -data["amount"]}},
)
if not sender:
return jsonify({"error": "Insufficient funds"}), 400
db.accounts.update(
{"user_id": data["to"]}, {"$inc": {"balance": data["amount"]}}
)
return jsonify({"status": "transferred"})
# ──────────────────────────────────────────────────────────────────────────────
# Secure logout with session destruction
# ──────────────────────────────────────────────────────────────────────────────
@app.post("/api/logout")
@login_required
def logout():
"""Destroy session and invalidate tokens on logout."""
user_id = session["user_id"]
db.tokens.delete_many({"user_id": user_id})
session.clear()
return jsonify({"status": "logged out"})
+215
View File
@@ -0,0 +1,215 @@
"""Vulnerable API fixture for testing TMT scanner detection capabilities.
This file intentionally contains security vulnerabilities across all
categories: replay attacks, race conditions, token abuse, auth/session
issues, and API route problems. Used exclusively for testing.
WARNING: This code is intentionally insecure. Never deploy in production.
"""
import hashlib
import random
import uuid
from flask import Flask, request, jsonify, session
app = Flask(__name__)
app.secret_key = "hardcoded-secret-key"
# ──────────────────────────────────────────────────────────────────────────────
# Global mutable state without synchronization (race condition + shared state)
# ──────────────────────────────────────────────────────────────────────────────
user_balances = {}
active_coupons = {}
# ──────────────────────────────────────────────────────────────────────────────
# Insecure session configuration
# ──────────────────────────────────────────────────────────────────────────────
app.config["SESSION_COOKIE_SECURE"] = False
app.config["SESSION_COOKIE_HTTPONLY"] = False
# ──────────────────────────────────────────────────────────────────────────────
# Overly permissive CORS
# ──────────────────────────────────────────────────────────────────────────────
@app.after_request
def add_cors(response):
"""Add wildcard CORS headers to every response."""
response.headers["Access-Control-Allow-Origin"] = "*"
return response
# ──────────────────────────────────────────────────────────────────────────────
# Login without session regeneration, weak password hash, no brute force protection
# ──────────────────────────────────────────────────────────────────────────────
@app.post("/api/login")
def login():
"""Authenticate a user with email and password."""
data = request.json
email = data["email"]
password_hash = hashlib.md5(data["password"].encode()).hexdigest()
user = db.users.find_one({"email": email, "password": password_hash})
if user:
session["user_id"] = str(user["_id"])
return jsonify({"status": "ok"})
return jsonify({"error": str("Invalid credentials")}), 401
# ──────────────────────────────────────────────────────────────────────────────
# Token generation: predictable, no expiry, no rate limit
# ──────────────────────────────────────────────────────────────────────────────
@app.post("/api/invite")
def generate_invite():
"""Generate an invitation token for a new user."""
token = str(uuid.uuid1())
db.invites.insert_one(
{
"token": token,
"created_by": session.get("user_id"),
}
)
return jsonify({"invite_token": token})
# ──────────────────────────────────────────────────────────────────────────────
# Invite acceptance without single-use enforcement (token reuse + replay)
# ──────────────────────────────────────────────────────────────────────────────
@app.post("/api/accept-invite")
def accept_invite():
"""Accept an invitation using a token."""
token = request.json["token"]
invite = db.invites.find_one({"token": token})
if not invite:
return jsonify({"error": "Invalid invite"}), 400
new_user = {"email": request.json["email"], "role": "member"}
db.users.insert_one(new_user)
return jsonify({"status": "account created"})
# ──────────────────────────────────────────────────────────────────────────────
# Balance transfer with race condition (non-atomic read-modify-write)
# ──────────────────────────────────────────────────────────────────────────────
@app.post("/api/transfer")
def transfer():
"""Transfer balance between user accounts."""
data = request.json
sender = db.accounts.find_one({"user_id": data["from"]})
if sender["balance"] >= data["amount"]:
db.accounts.update(
{"user_id": data["from"]},
{"$set": {"balance": sender["balance"] - data["amount"]}},
)
db.accounts.update(
{"user_id": data["to"]},
{"$set": {"balance": sender["balance"] + data["amount"]}},
)
return jsonify({"status": "transferred"})
# ──────────────────────────────────────────────────────────────────────────────
# Coupon redemption with race condition (TOCTOU)
# ──────────────────────────────────────────────────────────────────────────────
@app.post("/api/redeem-coupon")
def redeem_coupon():
"""Redeem a promotional coupon code."""
code = request.json["code"]
coupon = db.coupons.find_one({"code": code, "is_used": False})
if coupon:
apply_discount(coupon["discount"])
db.coupons.update({"code": code}, {"$set": {"is_used": True}})
return jsonify({"status": "redeemed"})
return jsonify({"error": "Invalid coupon"}), 400
# ──────────────────────────────────────────────────────────────────────────────
# Admin endpoint without role check
# ──────────────────────────────────────────────────────────────────────────────
@app.get("/api/admin/users")
def admin_list_users():
"""List all users in the system."""
users = list(db.users.find())
return jsonify(users)
# ──────────────────────────────────────────────────────────────────────────────
# Mass assignment vulnerability
# ──────────────────────────────────────────────────────────────────────────────
@app.put("/api/profile")
def update_profile():
"""Update the current user's profile."""
db.users.update({"_id": session["user_id"]}, {"$set": request.json})
return jsonify({"status": "updated"})
# ──────────────────────────────────────────────────────────────────────────────
# IDOR: object access without ownership check
# ──────────────────────────────────────────────────────────────────────────────
@app.get("/api/documents/<doc_id>")
def get_document(doc_id):
"""Retrieve a document by its ID."""
doc = db.documents.find_one({"_id": doc_id})
return jsonify(doc)
# ──────────────────────────────────────────────────────────────────────────────
# Verbose error exposure
# ──────────────────────────────────────────────────────────────────────────────
@app.post("/api/process")
def process_data():
"""Process submitted data."""
try:
result = complex_operation(request.json)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e), "trace": traceback.format_exc()}), 500
# ──────────────────────────────────────────────────────────────────────────────
# Logout that doesn't actually invalidate anything
# ──────────────────────────────────────────────────────────────────────────────
@app.post("/api/logout")
def logout():
"""Log the user out."""
return jsonify({"status": "logged out"})
# ──────────────────────────────────────────────────────────────────────────────
# Password reset with token but no invalidation after use
# ──────────────────────────────────────────────────────────────────────────────
@app.post("/api/reset-password")
def reset_password():
"""Reset a user's password using a reset token."""
token = request.json["token"]
result = verify_token(token)
if result:
new_hash = hashlib.sha1(request.json["new_password"].encode()).hexdigest()
db.users.update({"_id": result["user_id"]}, {"$set": {"password": new_hash}})
return jsonify({"status": "password reset"})
return jsonify({"error": "Invalid token"}), 400