mirror of
https://github.com/mytechnotalent/Threat-Modeling-Toolkit.git
synced 2026-09-30 07:51:42 +02:00
Initial commit: TMT lightweight threat modeling toolkit
This commit is contained in:
commit
717a0b819f
29 files changed
+4214
No files matched your search
Vendored
+1
@@ -0,0 +1 @@
|
||||
"""Test fixtures for TMT scanner validation."""
|
||||
Vendored
+174
@@ -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"})
|
||||
Vendored
+215
@@ -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
|
||||
Reference in new issue
Block a user