F-10/F-11 + Verwaltungsskript
- F-10 Open Redirect: is_safe_url()-Pruefung des next-Parameters beim Login; login_required gibt next weiter, Login-Formular traegt es als Hidden-Feld. Externe/protokoll-relative Ziele werden ignoriert -> Dashboard. - F-11 Info-Leak: eigene Fehlerseiten (400/403/404/405/500) ohne Framework- Hinweis oder Stacktrace (templates/error.html). - manage.sh: 'unlock' (Brute-Force-Sperren aufheben) und 'reset-password' (Admin-Passwort setzen/zufaellig erzeugen) via docker-compose run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+56
-3
@@ -7,7 +7,7 @@ import socket
|
||||
import sqlite3
|
||||
from datetime import datetime, timedelta
|
||||
from functools import wraps
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
from flask import (Flask, Response, abort, flash, redirect, render_template,
|
||||
request, session, url_for)
|
||||
@@ -82,6 +82,49 @@ def _inject_csrf():
|
||||
return {'csrf_token': lambda: session.get('_csrf_token', '')}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Eigene Fehlerseiten (verraten kein Framework, kein Stacktrace)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ERROR_MESSAGES = {
|
||||
400: 'Ungültige Anfrage.',
|
||||
403: 'Zugriff verweigert.',
|
||||
404: 'Seite nicht gefunden.',
|
||||
405: 'Methode nicht erlaubt.',
|
||||
500: 'Interner Serverfehler.',
|
||||
}
|
||||
|
||||
|
||||
def _render_error(code):
|
||||
return render_template('error.html', code=code,
|
||||
message=ERROR_MESSAGES.get(code, 'Fehler')), code
|
||||
|
||||
|
||||
@app.errorhandler(400)
|
||||
def _err_400(e):
|
||||
return _render_error(400)
|
||||
|
||||
|
||||
@app.errorhandler(403)
|
||||
def _err_403(e):
|
||||
return _render_error(403)
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def _err_404(e):
|
||||
return _render_error(404)
|
||||
|
||||
|
||||
@app.errorhandler(405)
|
||||
def _err_405(e):
|
||||
return _render_error(405)
|
||||
|
||||
|
||||
@app.errorhandler(500)
|
||||
def _err_500(e):
|
||||
return _render_error(500)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -90,11 +133,20 @@ def login_required(f):
|
||||
@wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
if 'admin_id' not in session:
|
||||
return redirect(url_for('login'))
|
||||
return redirect(url_for('login', next=request.path))
|
||||
return f(*args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
|
||||
def is_safe_url(target):
|
||||
"""True nur für relative Ziele auf derselben Origin (Open-Redirect-Schutz)."""
|
||||
if not target:
|
||||
return False
|
||||
ref = urlparse(request.host_url)
|
||||
test = urlparse(urljoin(request.host_url, target))
|
||||
return test.scheme in ('http', 'https') and ref.netloc == test.netloc
|
||||
|
||||
|
||||
# --- Brute-Force-Schutz (DB-gestützt, pro Scope + Client-IP) ---------------
|
||||
# Der Schlüssel ist "<scope>:<ip>", damit Admin-Login ('login') und
|
||||
# DynDNS-Update ('dyndns') getrennt gezählt werden.
|
||||
@@ -194,7 +246,8 @@ def login():
|
||||
session.clear()
|
||||
session['admin_id'] = admin['id']
|
||||
session['admin_username'] = admin['username']
|
||||
return redirect(url_for('dashboard'))
|
||||
nxt = request.form.get('next') or request.args.get('next')
|
||||
return redirect(nxt if is_safe_url(nxt) else url_for('dashboard'))
|
||||
|
||||
_record_login_fail('login', ip)
|
||||
flash('Benutzername oder Passwort falsch.', 'danger')
|
||||
|
||||
Reference in New Issue
Block a user