Fix XSS, CSRF, input validation, and related security issues
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user