Fix XSS, CSRF, input validation, and related security issues

This commit is contained in:
2026-04-14 16:00:50 -05:00
parent e0f04ea971
commit 270acc0430
7 changed files with 86 additions and 33 deletions
+40 -20
View File
@@ -3,11 +3,22 @@ from sqlalchemy import create_engine, func
from sqlalchemy.orm import scoped_session, sessionmaker
from datetime import datetime, date, timedelta
import json
import re
from models import Base, Battery, BatteryPctLog, Device, CapacityTest, ChargeLog
def _parse_date(val: str) -> str | None:
"""Return val if it is a valid YYYY-MM-DD string, else None."""
if not val:
return None
try:
datetime.strptime(val, "%Y-%m-%d")
return val
except ValueError:
return None
def create_app(config_object="config"):
app = Flask(__name__)
app.config.from_object(config_object)
@@ -28,6 +39,9 @@ def create_app(config_object="config"):
# Home Assistant integration (optional)
# ------------------------------------------------------------------ #
from flask_wtf.csrf import CSRFProtect
CSRFProtect(app)
from ha_client import HomeAssistantClient
from ha_poller import HaPoller
@@ -109,9 +123,14 @@ def create_app(config_object="config"):
purchase_date = f.get("purchase_date", "").strip() or None
storage_location = f.get("storage_location", "").strip() or None
existing = db.query(func.count(Battery.id)).filter_by(brand=brand).scalar()
existing_labels = [
r[0] for r in db.query(Battery.label).filter(Battery.brand == brand).all()
]
nums = [int(m.group(1)) for lbl in existing_labels
if (m := re.search(r'(\d+)$', lbl))]
next_num = max(nums, default=0)
for i in range(count):
label = f"{brand} {existing + i + 1:03d}"
label = f"{brand} {next_num + i + 1:03d}"
db.add(Battery(label=label, brand=brand, status="available", notes=notes,
size=size, chemistry=chemistry, capacity_mah=capacity_mah,
purchase_date=purchase_date, storage_location=storage_location))
@@ -160,26 +179,26 @@ def create_app(config_object="config"):
.filter_by(battery_id=battery_id)
.order_by(BatteryPctLog.recorded_at.desc())
.all())
charge_logs_json = json.dumps([
charge_logs_data = [
{"id": l.id, "date": l.charged_date, "cycles": l.increment_cycles, "notes": l.notes or ""}
for l in charge_logs
])
capacity_tests_json = json.dumps([
]
capacity_tests_data = [
{"id": t.id, "date": t.tested_date, "mah": t.tested_capacity_mah, "notes": t.notes or ""}
for t in sorted(capacity_tests, key=lambda t: (t.tested_date, t.id), reverse=True)
])
pct_logs_json = json.dumps([
]
pct_logs_data = [
{"recorded_at": str(l.recorded_at), "pct": l.percentage, "source": l.source or ""}
for l in pct_logs
])
]
return render_template("battery_detail.html", battery=battery,
storage_locations=storage_locations,
capacity_tests=capacity_tests,
charge_logs=charge_logs,
pct_logs=pct_logs,
charge_logs_json=charge_logs_json,
capacity_tests_json=capacity_tests_json,
pct_logs_json=pct_logs_json)
charge_logs_data=charge_logs_data,
capacity_tests_data=capacity_tests_data,
pct_logs_data=pct_logs_data)
# ------------------------------------------------------------------ #
# Battery — edit notes
@@ -204,7 +223,8 @@ def create_app(config_object="config"):
battery.chemistry = f.get("chemistry", "").strip() or None
battery.capacity_mah = _int("capacity_mah")
battery.charge_cycles = _int("charge_cycles")
battery.purchase_date = f.get("purchase_date", "").strip() or None
purchase_raw = f.get("purchase_date", "").strip()
battery.purchase_date = _parse_date(purchase_raw) if purchase_raw else None
battery.storage_location = f.get("storage_location", "").strip() or None
new_pct = _int("battery_percentage")
if new_pct != battery.battery_percentage:
@@ -239,10 +259,10 @@ def create_app(config_object="config"):
if battery is None:
abort(404)
mah_raw = request.form.get("tested_capacity_mah", "").strip()
date_val = request.form.get("tested_date", "").strip()
date_val = _parse_date(request.form.get("tested_date", "").strip())
notes = request.form.get("notes", "").strip() or None
if not mah_raw or not date_val:
flash("Capacity (mAh) and date are required.", "error")
flash("Capacity (mAh) and a valid date (YYYY-MM-DD) are required.", "error")
return redirect(url_for("battery_detail", battery_id=battery_id))
try:
mah = int(mah_raw)
@@ -281,9 +301,9 @@ def create_app(config_object="config"):
battery = db.get(Battery, battery_id)
if battery is None:
abort(404)
date_val = request.form.get("charged_date", "").strip()
date_val = _parse_date(request.form.get("charged_date", "").strip())
if not date_val:
flash("Date is required.", "error")
flash("A valid date (YYYY-MM-DD) is required.", "error")
return redirect(url_for("battery_detail", battery_id=battery_id))
increment = 1 if request.form.get("increment_cycles") else 0
notes = request.form.get("notes", "").strip() or None
@@ -544,9 +564,9 @@ def create_app(config_object="config"):
label = field_name.replace("_", " ").title()
flash(f"Set {label} on {n} batter{'y' if n == 1 else 'ies'}.", "success")
elif action == "log_charged":
date_val = request.form.get("charged_date", "").strip()
date_val = _parse_date(request.form.get("charged_date", "").strip())
if not date_val:
flash("Date is required.", "error")
flash("A valid date (YYYY-MM-DD) is required.", "error")
return redirect(url_for("dashboard"))
increment = 1 if request.form.get("increment_cycles") else 0
for b in batteries:
@@ -857,4 +877,4 @@ def create_app(config_object="config"):
app = create_app()
if __name__ == "__main__":
app.run(debug=True)
app.run(debug=False)