Security: XSS-Fix, localhost-Binding, API-Validierung (v1.1.3)
- Flask bindet auf 127.0.0.1 statt 0.0.0.0 — Port 8099 nicht mehr direkt im LAN erreichbar (host_network: true umgeht sonst HA-Auth) - XSS: esc() Funktion + HTML-Escaping für alle user-controlled Werte in innerHTML (inv.name, modbus_ip, mqtt_topic_prefix, s.name, s.unit) - API: POST /api/inverters-config validiert inverter_model, Port (1-65535), Modbus-Adresse (1-247) vor dem Speichern - _poll_loop: int()-Aufrufe in try/except — kein Thread-Crash bei ungültiger Config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
name: Growatt ShineLAN-X
|
name: Growatt ShineLAN-X
|
||||||
version: "1.1.2"
|
version: "1.1.3"
|
||||||
slug: growatt_shinelan_x
|
slug: growatt_shinelan_x
|
||||||
description: Growatt Wechselrichter via ShineLAN-X (NuttX Modbus TCP) - MQTT Discovery + Web UI
|
description: Growatt Wechselrichter via ShineLAN-X (NuttX Modbus TCP) - MQTT Discovery + Web UI
|
||||||
url: https://gitea.bitfire.work/retr0/Growatt-Wechselrichter-HAOS
|
url: https://gitea.bitfire.work/retr0/Growatt-Wechselrichter-HAOS
|
||||||
|
|||||||
+22
-3
@@ -87,12 +87,18 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
|
|||||||
inverter = INVERTERS.get(model_id, INVERTERS["MIC_1500_TL_X"])
|
inverter = INVERTERS.get(model_id, INVERTERS["MIC_1500_TL_X"])
|
||||||
prefix = inv_cfg.get("mqtt_topic_prefix", f"growatt/{inv_id}")
|
prefix = inv_cfg.get("mqtt_topic_prefix", f"growatt/{inv_id}")
|
||||||
device_id = f"growatt_{inv_id}"
|
device_id = f"growatt_{inv_id}"
|
||||||
|
try:
|
||||||
interval = max(5, int(inv_cfg.get("update_interval", 30)))
|
interval = max(5, int(inv_cfg.get("update_interval", 30)))
|
||||||
|
port = int(inv_cfg.get("modbus_port", 502))
|
||||||
|
slave = int(inv_cfg.get("modbus_address", 1))
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
log.error("[%s] Ungültige Konfiguration: %s", inv_id, e)
|
||||||
|
return
|
||||||
|
|
||||||
reader = ModbusReader(
|
reader = ModbusReader(
|
||||||
host=inv_cfg["modbus_ip"],
|
host=inv_cfg["modbus_ip"],
|
||||||
port=int(inv_cfg.get("modbus_port", 502)),
|
port=port,
|
||||||
slave=int(inv_cfg.get("modbus_address", 1)),
|
slave=slave,
|
||||||
)
|
)
|
||||||
|
|
||||||
with State.lock:
|
with State.lock:
|
||||||
@@ -202,6 +208,19 @@ def api_get_inverters():
|
|||||||
@app.post("/api/inverters-config")
|
@app.post("/api/inverters-config")
|
||||||
def api_save_inverters():
|
def api_save_inverters():
|
||||||
data = request.get_json(force=True) or []
|
data = request.get_json(force=True) or []
|
||||||
|
if not isinstance(data, list):
|
||||||
|
return jsonify({"error": "invalid"}), 400
|
||||||
|
for inv in data:
|
||||||
|
if not isinstance(inv, dict):
|
||||||
|
return jsonify({"error": "invalid"}), 400
|
||||||
|
if inv.get("inverter_model") not in INVERTERS:
|
||||||
|
return jsonify({"error": f"unknown model: {inv.get('inverter_model')}"}), 400
|
||||||
|
port = inv.get("modbus_port", 502)
|
||||||
|
if not isinstance(port, int) or not (1 <= port <= 65535):
|
||||||
|
return jsonify({"error": "invalid port"}), 400
|
||||||
|
addr = inv.get("modbus_address", 1)
|
||||||
|
if not isinstance(addr, int) or not (1 <= addr <= 247):
|
||||||
|
return jsonify({"error": "invalid modbus address (1-247)"}), 400
|
||||||
with State.lock:
|
with State.lock:
|
||||||
State.inverters_cfg = data
|
State.inverters_cfg = data
|
||||||
save_config()
|
save_config()
|
||||||
@@ -284,4 +303,4 @@ if __name__ == "__main__":
|
|||||||
_restart_all()
|
_restart_all()
|
||||||
port = int(os.environ.get("INGRESS_PORT", "8099"))
|
port = int(os.environ.get("INGRESS_PORT", "8099"))
|
||||||
log.info("Web UI startet auf Port %d", port)
|
log.info("Web UI startet auf Port %d", port)
|
||||||
app.run(host="0.0.0.0", port=port, threaded=True)
|
app.run(host="127.0.0.1", port=port, threaded=True)
|
||||||
|
|||||||
@@ -221,6 +221,10 @@
|
|||||||
<div class="toast" id="toast"></div>
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function esc(s) {
|
||||||
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||||
|
}
|
||||||
|
|
||||||
const DC_COLORS = {
|
const DC_COLORS = {
|
||||||
power: '#f0c040', voltage: '#58a6ff', current: '#ffa657',
|
power: '#f0c040', voltage: '#58a6ff', current: '#ffa657',
|
||||||
energy: '#3fb950', temperature: '#f85149', battery: '#bc8cff',
|
energy: '#3fb950', temperature: '#f85149', battery: '#bc8cff',
|
||||||
@@ -314,14 +318,14 @@ function renderLive(inverters) {
|
|||||||
const hist = (inv.history || {})[s.id] || [];
|
const hist = (inv.history || {})[s.id] || [];
|
||||||
return `<div class="sensor-card ${dcClass}">
|
return `<div class="sensor-card ${dcClass}">
|
||||||
<div class="sensor-icon">${ICON_MAP[s.icon]||"📊"}</div>
|
<div class="sensor-icon">${ICON_MAP[s.icon]||"📊"}</div>
|
||||||
<div class="sensor-name">${s.name}</div>
|
<div class="sensor-name">${esc(s.name)}</div>
|
||||||
<div class="sensor-value">${display}<span class="sensor-unit">${s.unit}</span></div>
|
<div class="sensor-value">${display}<span class="sensor-unit">${esc(s.unit)}</span></div>
|
||||||
${sparkline(hist, s.device_class)}
|
${sparkline(hist, s.device_class)}
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
return `<div class="inv-section">
|
return `<div class="inv-section">
|
||||||
<div class="inv-header">
|
<div class="inv-header">
|
||||||
<div class="inv-title">${inv.name}</div>
|
<div class="inv-title">${esc(inv.name)}</div>
|
||||||
<div class="inv-badge ${inv.modbus_ok ? "ok" : "err"}">${inv.modbus_ok ? "online" : "offline"}</div>
|
<div class="inv-badge ${inv.modbus_ok ? "ok" : "err"}">${inv.modbus_ok ? "online" : "offline"}</div>
|
||||||
<div class="info-chip" style="margin-left:auto;font-size:11px">⏱ ${ago} vor · ${inv.poll_count} Messungen</div>
|
<div class="info-chip" style="margin-left:auto;font-size:11px">⏱ ${ago} vor · ${inv.poll_count} Messungen</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -350,7 +354,7 @@ async function loadModels() {
|
|||||||
modelsList = await fetchJSON(api("api/inverter-models"));
|
modelsList = await fetchJSON(api("api/inverter-models"));
|
||||||
const sel = document.getElementById("modal-model");
|
const sel = document.getElementById("modal-model");
|
||||||
sel.innerHTML = Object.values(modelsList).map(m =>
|
sel.innerHTML = Object.values(modelsList).map(m =>
|
||||||
`<option value="${m.id}">${m.name} (${m.sensor_count} Sensoren)</option>`
|
`<option value="${esc(m.id)}">${esc(m.name)} (${m.sensor_count} Sensoren)</option>`
|
||||||
).join("");
|
).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,13 +366,13 @@ function renderInverterList() {
|
|||||||
<div class="inv-card-header">
|
<div class="inv-card-header">
|
||||||
<div class="inv-card-icon">☀️</div>
|
<div class="inv-card-icon">☀️</div>
|
||||||
<div class="inv-card-info">
|
<div class="inv-card-info">
|
||||||
<div class="inv-card-name">${inv.name || "Wechselrichter"}</div>
|
<div class="inv-card-name">${esc(inv.name || "Wechselrichter")}</div>
|
||||||
<div class="inv-card-model">${model.name || inv.inverter_model}</div>
|
<div class="inv-card-model">${esc(model.name || inv.inverter_model)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-card-meta">
|
<div class="inv-card-meta">
|
||||||
📡 ${inv.modbus_ip}:${inv.modbus_port || 502} · Slave ${inv.modbus_address || 1}<br>
|
📡 ${esc(inv.modbus_ip)}:${inv.modbus_port || 502} · Slave ${inv.modbus_address || 1}<br>
|
||||||
📨 ${inv.mqtt_topic_prefix}<br>
|
📨 ${esc(inv.mqtt_topic_prefix)}<br>
|
||||||
⏱ alle ${inv.update_interval || 30}s
|
⏱ alle ${inv.update_interval || 30}s
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-card-actions">
|
<div class="inv-card-actions">
|
||||||
|
|||||||
Reference in New Issue
Block a user