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:
+16
-4
@@ -55,10 +55,17 @@ app = Flask(__name__)
|
||||
app.secret_key = os.urandom(24)
|
||||
config_manager = ConfigManager()
|
||||
|
||||
# No CSRF protection: the UI is meant for the local network, where anyone who
|
||||
# can forge a request can also send it directly, and neither the HTMX forms
|
||||
# nor the fetch() calls carry a token. Exposing the UI beyond the LAN needs
|
||||
# CSRF tokens added to both first.
|
||||
# Cross-site request forgery: the UI has no login, and being "only on the LAN"
|
||||
# does not keep other websites out. Any page a LAN user opens can make their
|
||||
# browser POST to this server -- a plain HTML form is not blocked by CORS -- so
|
||||
# a hostile site could reboot the Pi, pull code or rewrite the config through
|
||||
# the user's browser. web_interface/origin_guard.py (registered below) refuses
|
||||
# POST/PUT/PATCH/DELETE whose Origin (or, failing that, Referer) is not this
|
||||
# server's own host; requests with neither header (curl, Home Assistant, the
|
||||
# MQTT bridge) are not from a browser and pass. There are no CSRF tokens:
|
||||
# neither the HTMX forms nor the fetch() calls carry one. Anyone who can reach
|
||||
# the port directly can still use the API, so exposing the UI beyond a trusted
|
||||
# network still needs real authentication.
|
||||
|
||||
# Initialize rate limiting (prevent accidental abuse, not security)
|
||||
try:
|
||||
@@ -402,6 +409,11 @@ def success_txt():
|
||||
from web_interface import request_logging
|
||||
request_logging.init_app(app)
|
||||
|
||||
# Refuse state-changing requests sent by another website's page (see the
|
||||
# cross-site note near the top of this file).
|
||||
from web_interface import origin_guard
|
||||
origin_guard.init_app(app)
|
||||
|
||||
# Global error handlers
|
||||
@app.errorhandler(404)
|
||||
def not_found_error(error):
|
||||
|
||||
Reference in New Issue
Block a user