Harden order endpoint and async mail; improve security defaults
This commit is contained in:
52
wawi/app.py
52
wawi/app.py
@@ -4,6 +4,8 @@ import os
|
||||
import sqlite3
|
||||
import secrets
|
||||
import smtplib
|
||||
import time
|
||||
import threading
|
||||
from uuid import uuid4
|
||||
from email.message import EmailMessage
|
||||
from functools import wraps
|
||||
@@ -28,12 +30,18 @@ app = Flask(__name__, static_url_path=STATIC_URL_PATH)
|
||||
# Session‑Secret für Login‑Cookies (in Produktion unbedingt setzen).
|
||||
app.secret_key = os.environ.get("SECRET_KEY", "change-me")
|
||||
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
|
||||
app.config["SESSION_COOKIE_SECURE"] = False
|
||||
app.config["SESSION_COOKIE_SECURE"] = os.environ.get("COOKIE_SECURE", "1") == "1"
|
||||
app.config["SESSION_COOKIE_HTTPONLY"] = True
|
||||
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024
|
||||
bp = Blueprint("bp", __name__)
|
||||
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
_DB_INIT_DONE = False
|
||||
_RATE_LIMIT = {}
|
||||
_RATE_WINDOW = 60
|
||||
_RATE_MAX = 15
|
||||
|
||||
|
||||
def get_db() -> sqlite3.Connection:
|
||||
if "db" not in g:
|
||||
@@ -146,8 +154,11 @@ def ensure_orders_columns(db: sqlite3.Connection) -> None:
|
||||
|
||||
@app.before_request
|
||||
def ensure_db() -> None:
|
||||
# Erstellt Tabellen, wenn sie fehlen (bei jedem Request idempotent).
|
||||
init_db()
|
||||
# Erstellt Tabellen einmal pro Worker.
|
||||
global _DB_INIT_DONE
|
||||
if not _DB_INIT_DONE:
|
||||
init_db()
|
||||
_DB_INIT_DONE = True
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
@@ -160,6 +171,8 @@ def save_upload(file) -> str | None:
|
||||
ext = os.path.splitext(file.filename)[1].lower()
|
||||
if ext not in ALLOWED_EXT:
|
||||
return None
|
||||
if not (file.mimetype or "").startswith("image/"):
|
||||
return None
|
||||
name = secure_filename(Path(file.filename).stem) or "image"
|
||||
safe_name = f"{name}-{uuid4().hex}{ext}"
|
||||
dest = UPLOAD_DIR / safe_name
|
||||
@@ -214,6 +227,16 @@ def api_key_required(fn):
|
||||
return wrapper
|
||||
|
||||
|
||||
def rate_limited(ip: str) -> bool:
|
||||
now = time.time()
|
||||
bucket = _RATE_LIMIT.setdefault(ip, [])
|
||||
bucket[:] = [t for t in bucket if now - t < _RATE_WINDOW]
|
||||
if len(bucket) >= _RATE_MAX:
|
||||
return True
|
||||
bucket.append(now)
|
||||
return False
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
@login_required
|
||||
def index():
|
||||
@@ -534,10 +557,22 @@ def build_bestand() -> list[dict]:
|
||||
|
||||
@bp.route("/order", methods=["POST"])
|
||||
def order():
|
||||
ip = request.headers.get("X-Forwarded-For", request.remote_addr or "unknown").split(",")[0].strip()
|
||||
if rate_limited(ip):
|
||||
return jsonify({"error": "Zu viele Anfragen."}), 429
|
||||
|
||||
expected_key = os.environ.get("ORDER_API_KEY", "")
|
||||
if expected_key:
|
||||
provided = request.headers.get("X-Order-Key") or request.args.get("key") or ""
|
||||
if provided != expected_key:
|
||||
return jsonify({"error": "Unauthorized"}), 401
|
||||
|
||||
data = request.get_json(silent=True) or request.form
|
||||
required = ["name", "handy", "mannschaft", "artikel", "groesse", "menge"]
|
||||
if any(not (data.get(k) or "").strip() for k in required):
|
||||
return jsonify({"error": "Pflichtfelder fehlen."}), 400
|
||||
if int(data.get("menge") or 0) <= 0:
|
||||
return jsonify({"error": "Menge muss größer als 0 sein."}), 400
|
||||
|
||||
db = get_db()
|
||||
db.execute(
|
||||
@@ -586,10 +621,13 @@ def order():
|
||||
)
|
||||
msg.set_content(body)
|
||||
|
||||
with smtplib.SMTP(smtp_host, smtp_port) as server:
|
||||
server.starttls()
|
||||
server.login(smtp_user, smtp_pass)
|
||||
server.send_message(msg, to_addrs=recipients)
|
||||
def _send():
|
||||
with smtplib.SMTP(smtp_host, smtp_port) as server:
|
||||
server.starttls()
|
||||
server.login(smtp_user, smtp_pass)
|
||||
server.send_message(msg, to_addrs=recipients)
|
||||
|
||||
threading.Thread(target=_send, daemon=True).start()
|
||||
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user