Security hardening + bug fixes: rotate admin pw, /api/v1/dedicated/locations alias (404 fix), rate-limit login/signup, SameSite+HttpOnly session cookies, protect /dedicated/poll (403), whitelist admin.py set_field

This commit is contained in:
drjones
2026-09-20 12:18:05 -07:00
parent 0cb80be87f
commit e94c5cfd0c
2 changed files with 28 additions and 2 deletions

24
app.py
View File

@@ -17,6 +17,8 @@ import iproyal
CFG = db.CFG
app = Flask(__name__)
app.secret_key = CFG["secret_key"]
app.config["SESSION_COOKIE_SAMESITE"] = "Lax" # mitigate CSRF on modern browsers
app.config["SESSION_COOKIE_HTTPONLY"] = True # block JS session theft (XSS)
# simple SQLite-backed rate limiter (shared across gunicorn workers)
def _client_ip():
@@ -129,6 +131,9 @@ def index():
@app.route("/signup", methods=["GET", "POST"])
def signup():
if request.method == "POST":
if not _rate_ok(_client_ip(), limit=20, window=300):
flash("Too many attempts. Slow down.", "warn")
return render_template("signup.html")
username = request.form.get("username", "").strip()
password = request.form.get("password", "")
if len(username) < 3 or len(password) < 6:
@@ -158,6 +163,9 @@ def signup():
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
if not _rate_ok(_client_ip(), limit=20, window=300):
flash("Too many attempts. Slow down.", "warn")
return render_template("login.html")
username = request.form.get("username", "").strip()
password = request.form.get("password", "")
d = db.get_db()
@@ -377,6 +385,16 @@ def dedicated_locations():
return jsonify({"error": str(e)}), 502
@app.route("/api/v1/dedicated/locations")
def api_dedicated_locations():
pid = request.args.get("product", 9, type=int)
try:
return jsonify({"locations": [{"id": i, "name": n, "in_stock": s}
for i, n, s in iproyal.list_locations(pid)]})
except Exception as e:
return jsonify({"error": str(e)}), 502
@app.route("/dedicated/buy", methods=["POST"])
@login_required
def dedicated_buy():
@@ -405,7 +423,9 @@ def dedicated_buy():
@app.route("/dedicated/poll", methods=["POST"])
def dedicated_poll():
"""Admin/agent hook: poll provisioning for pending dedicated orders."""
"""Admin hook: poll provisioning for pending dedicated orders."""
if not session.get("admin"):
abort(403)
ok, failed = poll_provisioning()
return jsonify({"provisioned": ok, "failed": failed})
@@ -457,7 +477,7 @@ def poll_provisioning():
# ---------- admin ----------
ADMIN_PASSWORD = CFG.get("admin_password", "czapiewski")
ADMIN_PASSWORD = CFG.get("admin_password", "")
@app.route("/admin", methods=["GET", "POST"])