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:
6
admin.py
6
admin.py
@@ -32,7 +32,13 @@ def set_country(username, country):
|
|||||||
print(f"OK {username} country={country}")
|
print(f"OK {username} country={country}")
|
||||||
|
|
||||||
|
|
||||||
|
ALLOWED_FIELDS = {"country", "city", "region", "sticky", "active", "balance_gb"}
|
||||||
|
|
||||||
|
|
||||||
def set_field(username, field, value):
|
def set_field(username, field, value):
|
||||||
|
if field not in ALLOWED_FIELDS:
|
||||||
|
print(f"ERROR: field '{field}' not allowed (whitelist: {sorted(ALLOWED_FIELDS)})")
|
||||||
|
return
|
||||||
d = db.get_db()
|
d = db.get_db()
|
||||||
d.execute(f"UPDATE users SET {field}=? WHERE username=?", (value, username))
|
d.execute(f"UPDATE users SET {field}=? WHERE username=?", (value, username))
|
||||||
d.commit()
|
d.commit()
|
||||||
|
|||||||
24
app.py
24
app.py
@@ -17,6 +17,8 @@ import iproyal
|
|||||||
CFG = db.CFG
|
CFG = db.CFG
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = CFG["secret_key"]
|
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)
|
# simple SQLite-backed rate limiter (shared across gunicorn workers)
|
||||||
def _client_ip():
|
def _client_ip():
|
||||||
@@ -129,6 +131,9 @@ def index():
|
|||||||
@app.route("/signup", methods=["GET", "POST"])
|
@app.route("/signup", methods=["GET", "POST"])
|
||||||
def signup():
|
def signup():
|
||||||
if request.method == "POST":
|
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()
|
username = request.form.get("username", "").strip()
|
||||||
password = request.form.get("password", "")
|
password = request.form.get("password", "")
|
||||||
if len(username) < 3 or len(password) < 6:
|
if len(username) < 3 or len(password) < 6:
|
||||||
@@ -158,6 +163,9 @@ def signup():
|
|||||||
@app.route("/login", methods=["GET", "POST"])
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
if request.method == "POST":
|
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()
|
username = request.form.get("username", "").strip()
|
||||||
password = request.form.get("password", "")
|
password = request.form.get("password", "")
|
||||||
d = db.get_db()
|
d = db.get_db()
|
||||||
@@ -377,6 +385,16 @@ def dedicated_locations():
|
|||||||
return jsonify({"error": str(e)}), 502
|
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"])
|
@app.route("/dedicated/buy", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def dedicated_buy():
|
def dedicated_buy():
|
||||||
@@ -405,7 +423,9 @@ def dedicated_buy():
|
|||||||
|
|
||||||
@app.route("/dedicated/poll", methods=["POST"])
|
@app.route("/dedicated/poll", methods=["POST"])
|
||||||
def dedicated_poll():
|
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()
|
ok, failed = poll_provisioning()
|
||||||
return jsonify({"provisioned": ok, "failed": failed})
|
return jsonify({"provisioned": ok, "failed": failed})
|
||||||
|
|
||||||
@@ -457,7 +477,7 @@ def poll_provisioning():
|
|||||||
|
|
||||||
|
|
||||||
# ---------- admin ----------
|
# ---------- admin ----------
|
||||||
ADMIN_PASSWORD = CFG.get("admin_password", "czapiewski")
|
ADMIN_PASSWORD = CFG.get("admin_password", "")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/admin", methods=["GET", "POST"])
|
@app.route("/admin", methods=["GET", "POST"])
|
||||||
|
|||||||
Reference in New Issue
Block a user