mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-06 15:25:08 +00:00
fix(web): refuse cross-site state-changing requests (Origin/Referer check)
The web interface had no CSRF protection, on the reasoning that anyone who can forge a request on the LAN can also send it directly. That misses the browser as a confused deputy: any website a LAN user opens can make their browser POST a plain HTML form to http://<pi>:5000. CORS does not stop that request, only hides its answer, and /api/v3/system/action accepted form bodies, so a hostile page could reboot or power off the Pi, pull code, or reach any other mutating route. - web_interface/origin_guard.py: an app-wide before_request hook refuses POST/PUT/PATCH/DELETE whose Origin (or, without one, Referer) is not the host the request was addressed to, and Origin "null", with 403 CROSS_SITE_REQUEST. Requests with neither header (curl, Home Assistant, the MQTT bridge) are not from a browser and pass. Host and port are compared, not the scheme, so a TLS proxy that passes Host through works; X-Forwarded-Host is not trusted (no ProxyFix). - /api/v3/system/action refuses a non-JSON body (415) unless HX-Request is set; every caller in the interface already sends JSON. - app.py comment states the real threat model; SECURITY.md, REST_API_REFERENCE.md, WEB_INTERFACE_GUIDE.md and CHANGELOG updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -383,16 +383,27 @@ def _perform_core_update_locked(stash_local_changes=True):
|
||||
def execute_system_action():
|
||||
"""Execute system actions (start/stop/reboot/etc)"""
|
||||
try:
|
||||
# HTMX sends data as form data, not JSON
|
||||
data = request.get_json(silent=True) or {}
|
||||
if not data:
|
||||
# Try to get from form data if JSON fails
|
||||
data = request.get_json(silent=True)
|
||||
if data is None and not request.is_json:
|
||||
# Every caller in the interface sends JSON (the Quick Actions
|
||||
# buttons use HTMX's json-enc). A form-encoded body is what a
|
||||
# cross-site HTML form can send without a CORS preflight, and
|
||||
# this route reboots, powers off and pulls code, so it is only
|
||||
# accepted from HTMX: a cross-site form cannot set HX-Request.
|
||||
# This backs up the app-wide Origin check (origin_guard.py).
|
||||
if not request.headers.get('HX-Request'):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': ('Send the action as JSON '
|
||||
'(Content-Type: application/json), '
|
||||
'e.g. {"action": "restart_display_service"}'),
|
||||
}), 415
|
||||
data = {
|
||||
'action': request.form.get('action'),
|
||||
'mode': request.form.get('mode')
|
||||
}
|
||||
|
||||
if not data or 'action' not in data:
|
||||
if not isinstance(data, dict) or not data.get('action'):
|
||||
return jsonify({'status': 'error', 'message': 'Action required'}), 400
|
||||
|
||||
action = data['action']
|
||||
|
||||
Reference in New Issue
Block a user