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')
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ code }} — DynDNS Manager</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}">
|
||||
</head>
|
||||
<body class="justify-content-center">
|
||||
<div class="login-card text-center">
|
||||
<div class="card shadow">
|
||||
<div class="card-body p-5">
|
||||
<div class="display-4 fw-bold text-primary mb-2">{{ code }}</div>
|
||||
<p class="text-muted mb-4">{{ message }}</p>
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-primary">Zur Startseite</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -23,6 +23,7 @@
|
||||
{% endwith %}
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="next" value="{{ request.args.get('next', '') }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Benutzername</label>
|
||||
<input name="username" type="text" class="form-control" autofocus required>
|
||||
|
||||
Reference in New Issue
Block a user