security: add CSRF protection to all forms
- Add Flask-WTF dependency for CSRF protection - Initialize CSRFProtect in app.py - Add CSRF tokens to all POST forms in templates - Exempt /order JSON API endpoint (uses API key instead) This protects against Cross-Site Request Forgery attacks on all admin and user management operations. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
4
requirements.txt
Executable file
4
requirements.txt
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
Flask>=3.0.0
|
||||||
|
Flask-WTF>=1.2.0
|
||||||
|
Werkzeug>=3.0.0
|
||||||
|
gunicorn>=21.0.0
|
||||||
@@ -22,6 +22,7 @@ from datetime import datetime
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from flask import Flask, Blueprint, g, flash, jsonify, redirect, render_template, request, session, url_for
|
from flask import Flask, Blueprint, g, flash, jsonify, redirect, render_template, request, session, url_for
|
||||||
|
from flask_wtf.csrf import CSRFProtect
|
||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
@@ -41,6 +42,10 @@ app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
|
|||||||
app.config["SESSION_COOKIE_SECURE"] = os.environ.get("COOKIE_SECURE", "1") == "1"
|
app.config["SESSION_COOKIE_SECURE"] = os.environ.get("COOKIE_SECURE", "1") == "1"
|
||||||
app.config["SESSION_COOKIE_HTTPONLY"] = True
|
app.config["SESSION_COOKIE_HTTPONLY"] = True
|
||||||
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024
|
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024
|
||||||
|
|
||||||
|
# CSRF-Schutz aktivieren
|
||||||
|
csrf = CSRFProtect(app)
|
||||||
|
|
||||||
bp = Blueprint("bp", __name__)
|
bp = Blueprint("bp", __name__)
|
||||||
|
|
||||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -597,6 +602,7 @@ def build_bestand() -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
@bp.route("/order", methods=["POST"])
|
@bp.route("/order", methods=["POST"])
|
||||||
|
@csrf.exempt # JSON API ohne CSRF-Schutz (nutzt API-Key stattdessen)
|
||||||
def order():
|
def order():
|
||||||
"""Erstellt eine Bestellung (optional API‑Key) und versendet Mail."""
|
"""Erstellt eine Bestellung (optional API‑Key) und versendet Mail."""
|
||||||
ip = request.headers.get("X-Forwarded-For", request.remote_addr or "unknown").split(",")[0].strip()
|
ip = request.headers.get("X-Forwarded-For", request.remote_addr or "unknown").split(",")[0].strip()
|
||||||
|
|||||||
1
wawi/templates/ausbuchen.html
Normal file → Executable file
1
wawi/templates/ausbuchen.html
Normal file → Executable file
@@ -4,6 +4,7 @@
|
|||||||
<h2>Ausbuchen: {{ item.artikel }} ({{ item.groesse }})</h2>
|
<h2>Ausbuchen: {{ item.artikel }} ({{ item.groesse }})</h2>
|
||||||
<div class="note">Aktueller Bestand: <strong>{{ item.gezaehlt }}</strong></div>
|
<div class="note">Aktueller Bestand: <strong>{{ item.gezaehlt }}</strong></div>
|
||||||
<form method="post" onsubmit="return confirm('Wirklich ausbuchen?');">
|
<form method="post" onsubmit="return confirm('Wirklich ausbuchen?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label>
|
<label>
|
||||||
Menge
|
Menge
|
||||||
|
|||||||
1
wawi/templates/edit.html
Normal file → Executable file
1
wawi/templates/edit.html
Normal file → Executable file
@@ -3,6 +3,7 @@
|
|||||||
<div class="card form-card">
|
<div class="card form-card">
|
||||||
<h2>{{ "Artikel bearbeiten" if item else "Neuen Artikel anlegen" }}</h2>
|
<h2>{{ "Artikel bearbeiten" if item else "Neuen Artikel anlegen" }}</h2>
|
||||||
<form method="post" enctype="multipart/form-data">
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label>
|
<label>
|
||||||
Artikel
|
Artikel
|
||||||
|
|||||||
2
wawi/templates/index.html
Normal file → Executable file
2
wawi/templates/index.html
Normal file → Executable file
@@ -67,10 +67,12 @@
|
|||||||
<td class="actions">
|
<td class="actions">
|
||||||
<a class="btn icon" href="{{ url_for('bp.edit_item', item_id=r.id) }}" title="Bearbeiten" aria-label="Bearbeiten"><span>✎</span></a>
|
<a class="btn icon" href="{{ url_for('bp.edit_item', item_id=r.id) }}" title="Bearbeiten" aria-label="Bearbeiten"><span>✎</span></a>
|
||||||
<form method="post" action="{{ url_for('bp.verkauf', item_id=r.id) }}" onsubmit="return confirm('Wirklich 1 Stück als verkauft buchen?');">
|
<form method="post" action="{{ url_for('bp.verkauf', item_id=r.id) }}" onsubmit="return confirm('Wirklich 1 Stück als verkauft buchen?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button class="btn icon" type="submit" title="Verkauf +1" aria-label="Verkauf +1"><span>🛒</span></button>
|
<button class="btn icon" type="submit" title="Verkauf +1" aria-label="Verkauf +1"><span>🛒</span></button>
|
||||||
</form>
|
</form>
|
||||||
<a class="btn icon" href="{{ url_for('bp.ausbuchen', item_id=r.id) }}" title="Ausbuchen" aria-label="Ausbuchen"><span>⇩</span></a>
|
<a class="btn icon" href="{{ url_for('bp.ausbuchen', item_id=r.id) }}" title="Ausbuchen" aria-label="Ausbuchen"><span>⇩</span></a>
|
||||||
<form method="post" action="{{ url_for('bp.delete_item', item_id=r.id) }}" onsubmit="return confirm('Wirklich löschen? Dieser Vorgang kann nicht rückgängig gemacht werden.');">
|
<form method="post" action="{{ url_for('bp.delete_item', item_id=r.id) }}" onsubmit="return confirm('Wirklich löschen? Dieser Vorgang kann nicht rückgängig gemacht werden.');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button class="btn icon danger" type="submit" title="Löschen" aria-label="Löschen"><span>🗑</span></button>
|
<button class="btn icon danger" type="submit" title="Löschen" aria-label="Löschen"><span>🗑</span></button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
1
wawi/templates/login.html
Normal file → Executable file
1
wawi/templates/login.html
Normal file → Executable file
@@ -6,6 +6,7 @@
|
|||||||
<div class="note">Benutzername oder Passwort ist falsch.</div>
|
<div class="note">Benutzername oder Passwort ist falsch.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label>
|
<label>
|
||||||
Benutzer
|
Benutzer
|
||||||
|
|||||||
2
wawi/templates/orders.html
Normal file → Executable file
2
wawi/templates/orders.html
Normal file → Executable file
@@ -45,9 +45,11 @@
|
|||||||
<td class="actions">
|
<td class="actions">
|
||||||
{% if not o.done and not o.canceled %}
|
{% if not o.done and not o.canceled %}
|
||||||
<form method="post" action="{{ url_for('bp.complete_order', order_id=o.id) }}" onsubmit="return confirm('Bestellung als erledigt markieren?');">
|
<form method="post" action="{{ url_for('bp.complete_order', order_id=o.id) }}" onsubmit="return confirm('Bestellung als erledigt markieren?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button class="btn small" type="submit">Erledigt</button>
|
<button class="btn small" type="submit">Erledigt</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="{{ url_for('bp.cancel_order', order_id=o.id) }}" onsubmit="return confirm('Bestellung wirklich stornieren?');">
|
<form method="post" action="{{ url_for('bp.cancel_order', order_id=o.id) }}" onsubmit="return confirm('Bestellung wirklich stornieren?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button class="btn small danger" type="submit">Stornieren</button>
|
<button class="btn small danger" type="submit">Stornieren</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
3
wawi/templates/users.html
Normal file → Executable file
3
wawi/templates/users.html
Normal file → Executable file
@@ -15,6 +15,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label>
|
<label>
|
||||||
Benutzername
|
Benutzername
|
||||||
@@ -47,9 +48,11 @@
|
|||||||
<td>{{ u.created_at }}</td>
|
<td>{{ u.created_at }}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<form method="post" action="{{ url_for('bp.reset_user_password', user_id=u.id) }}" onsubmit="return confirm('Passwort für diesen Benutzer wirklich zurücksetzen?');">
|
<form method="post" action="{{ url_for('bp.reset_user_password', user_id=u.id) }}" onsubmit="return confirm('Passwort für diesen Benutzer wirklich zurücksetzen?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button class="btn small" type="submit">Passwort neu</button>
|
<button class="btn small" type="submit">Passwort neu</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="{{ url_for('bp.delete_user', user_id=u.id) }}" onsubmit="return confirm('Benutzer wirklich löschen?');">
|
<form method="post" action="{{ url_for('bp.delete_user', user_id=u.id) }}" onsubmit="return confirm('Benutzer wirklich löschen?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button class="btn small danger" type="submit">Löschen</button>
|
<button class="btn small danger" type="submit">Löschen</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user