From 3c6b7d5cee9061a4315d483321abc13a474bb50f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Jul 2026 19:46:01 +0000 Subject: [PATCH] security: fix bugs, add auth, TLS, input validation; update deps and Docker image --- .env.example | 26 +++++++++++++ .gitignore | 1 + Dockerfile | 12 +++--- app/__pycache__/main.cpython-312.pyc | Bin 0 -> 5284 bytes app/main.py | 56 ++++++++++++++++++++------- docker-compose.yml | 18 +++++---- requirements.txt | 3 ++ 7 files changed, 87 insertions(+), 29 deletions(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 app/__pycache__/main.cpython-312.pyc create mode 100644 requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a6942bb --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +# Copy this file to .env and fill in your values. +# .env is listed in .gitignore and will NOT be committed. + +# ----- MQTT settings ----- +MQTT_SERVER=192.168.1.1 +MQTT_PORT=1883 +MQTT_PATH=homeassistant/webhook + +# Optional: MQTT broker credentials +# MQTT_USER=myuser +# MQTT_PASSWORD=mypassword + +# Optional: Enable TLS for MQTT connection (1 / true / yes) +# MQTT_TLS=false + +# ----- Webhook authentication ----- +# If set, every incoming POST must include: +# Authorization: Bearer +# Leave empty to disable authentication (not recommended in production). +# WEBHOOK_TOKEN=change-me-to-a-strong-random-secret + +# ----- Timezone ----- +TZ=Europe/Berlin + +# ----- Traefik labels (adapt to your domain) ----- +WEBHOOK_HOST=webhook.example.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/Dockerfile b/Dockerfile index e1fc552..f943205 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ -FROM tiangolo/meinheld-gunicorn-flask:latest +FROM python:3.12-slim -RUN apt-get update && apt-get install -y \ - python3-pip +WORKDIR /app -RUN /usr/local/bin/python -m pip install --upgrade pip -RUN pip3 install --upgrade Flask -RUN pip3 install paho-mqtt +COPY requirements.txt . +RUN python -m pip install --no-cache-dir -r requirements.txt COPY ./app /app + +CMD ["gunicorn", "--bind", "0.0.0.0:80", "main:app"] diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..951984c08d6a4e32124e5b0069855ff7fe5170d7 GIT binary patch literal 5284 zcmbVQUrZZE8lUywwRa8Xe}G`jpA<|2s3C1GNlQZr2_)2{;3QX96>``G?ATs+c7Z_P z&gdcmPYF^bc<4hS&>q#fwi}&oxWI|Q0cQ1L%H7}>!%YC!m=Q@wgE@f3%Or<^+WPP8;m87nv7lf!jh7lDIfZaA7WC30o4@u$7}S zOWYQ=8Pp!O8`P0-PFXmV;KR;eS<|asbph)hpQp-aZ|%MxtdvU2S5#(OB-1r|4%+@#V`scD9L>|=S9n(rZ& zxA2KYOThn(8V9VzekmSNZxbh$KbezNjqu~Lno^Rg93b(}1BdY9IWz8=q=c8Cj zCgh~neItUSky~-uKdoT@0f%Q4VwY;D`2ne4Q~Y;Vh|3UN8ygQrKXQUuRmqIXUNssB z4HM@=VG4zKiBB}GI~CDpsbR<6m5yS(>o8Q}DTE%Fn7BDMev$A-Gc-CuXqLD)iA}?E zGO;eo>QW=s`6F}MEKDRi715$f66Qu{BQ1FiB{(t(@!TA`Fg!MPbuu(|btt&hUQ8Khyva7e&iZ>giF?1ZEQx8D~<>xK7Y^tjv@$F6L#NN!c7% zIIWUNal2G$&@or^OZP12XA?*Z{w|Dz%8IV)(_DF7vO_S&IJYEj%sI#i=vULMG~oJ zbQfS+G(rIjv3{bc#DQfk5=}0(UW!EFT+$<-n!6Q`s0Q0)-E2kwy5k$6dRMH>i>U$Dd^F#fR;`>-ufi&W&vAsV`c-ytrPL z<3~W5=j(O8{%gMRUwcCJ_lTpzgAn3|n5j9FuQ{*Rod2>STQivFFY5foe?PZUiR5Y-VXdru9w)ZjZK6C9C0ROZB~)gDX=d+^y7wR{)PL0 zKw=Y7&2ytsEH@rJZ|Y+_lbSSELQGR8agv20*9rn`wZ=TAmT_hutl!(P$hiN&*)M0d zCY5Z5#wyin26R6>mwqe2OxtO$;# z!VPQ7e4NEgKMv6D=>>q;qd8Rn+<`p4^~-s& zNf(9vQ8upPRk1BLj@t`$_YQWr9@!-PNmCY-<)wf$&z16kZwf(VP zd3^QiK5XOB)zM7Fg`910*CwV1*M?Vz*FU&-l@1Cz&4_Jzu}c@bcFtwR(>e5ueQVi+ zRvJOK9)04iD#YEst5+Y-dQYs5?0c)zBPHLKyx6IWojdheu`h>C9rWGxR&KdB-P`AM zZwGa&Kj6l?Jd+jQ%AvRS#j^WXHm=b6yA7l0fXNn|x5_rlwtt=V_NG0%g6IC(jkEXr zH~P1`KEIU}0(qfV7kab8o0+#pGQvoje_|DP3#T^Y?atYHo>+xayESKP+yAeiPpsaO z#oJ%oPSb1!SA&_V>p9zvzd3~U;om4}>u%f0v~|tB>dpwwI`V(-DC35>=dH+MU#nlO dU+>$l-R9HvS+3=u77yotVMUf!gERKie*n>KPTK$g literal 0 HcmV?d00001 diff --git a/app/main.py b/app/main.py index fcab247..3549f32 100644 --- a/app/main.py +++ b/app/main.py @@ -4,22 +4,19 @@ import paho.mqtt.client as mqtt import json import datetime - import os -#$env:MQTT_SERVER='192.168.176.6' +import sys + #-------------Output Logger # create logger logger = logging.getLogger("Webhook2MQTT") -#logger.setLevel(logging.INFO) logger.setLevel(logging.INFO) # create console handler with a higher log level ch = logging.StreamHandler() -#ch.setLevel(logging.INFO) ch.setLevel(logging.INFO) # create formatter and add it to the handlers formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') -#formatter = logging.Formatter('%(levelname)s - %(message)s') ch.setFormatter(formatter) # add the handlers to the logger logger.addHandler(ch) @@ -29,7 +26,7 @@ mqtt_server = os.environ['MQTT_SERVER'] else: logger.error("Please set environment-Variable for MQTT_SERVER") - os.exit() + sys.exit(1) logger.info("set MQTT_SERVER to {}".format(mqtt_server)) if 'MQTT_PORT' in os.environ: @@ -44,25 +41,54 @@ mqtt_path = 'webhook' logger.info("set mqtt-path to '{}'".format(mqtt_path)) +mqtt_user = os.environ.get('MQTT_USER') +mqtt_password = os.environ.get('MQTT_PASSWORD') +mqtt_tls = os.environ.get('MQTT_TLS', '').lower() in ('1', 'true', 'yes') +if mqtt_user: + logger.info("MQTT authentication enabled") +if mqtt_tls: + logger.info("MQTT TLS enabled") + +webhook_token = os.environ.get('WEBHOOK_TOKEN') +if webhook_token: + logger.info("Webhook token authentication enabled") +else: + logger.warning("WEBHOOK_TOKEN is not set — webhook endpoint is unauthenticated") app = Flask(__name__) +app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024 # 1 MB limit + def workit(params): logger.info("workit:") - logger.info(params) - params['timestamp'] = datetime.datetime.now().isoformat() - client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1) - client.connect(mqtt_server, mqtt_port, 60) - client.publish(mqtt_path, json.dumps(params),qos=0,retain=True) - client.disconnect() + logger.info(params) + try: + params['timestamp'] = datetime.datetime.now().isoformat() + client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + if mqtt_user: + client.username_pw_set(mqtt_user, mqtt_password) + if mqtt_tls: + client.tls_set() + client.connect(mqtt_server, mqtt_port, 60) + client.publish(mqtt_path, json.dumps(params), qos=0, retain=True) + client.disconnect() + except Exception as e: + logger.error("Failed to publish MQTT message: {}".format(e)) + @app.route('/', methods=['POST']) def respond(): logger.info(request) - myparams=request.get_json() + if webhook_token: + auth_header = request.headers.get('Authorization', '') + if auth_header != 'Bearer {}'.format(webhook_token): + logger.warning("Unauthorized request from {}".format(request.remote_addr)) + return Response(status=401) + myparams = request.get_json() + if myparams is None: + logger.warning("Received non-JSON or empty request body") + return Response(status=400) x = threading.Thread(target=workit, args=(myparams,)) x.start() return Response(status=200) -#app.run (host = "localhost", port = 5050) - diff --git a/docker-compose.yml b/docker-compose.yml index 7a55654..1e65a3b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3.6" services: webhook2mqtt: build: . @@ -11,24 +10,27 @@ services: - "/etc/localtime:/etc/localtime:ro" - "/etc/timezone:/etc/timezone:ro" environment: - TZ: "Europe/Berlin" - MQTT_SERVER: 'home.jru.me' - MQTT_PORT: 1883 - MQTT_PATH: 'homeassistant/webhook' + TZ: ${TZ:-Europe/Berlin} + MQTT_SERVER: ${MQTT_SERVER} + MQTT_PORT: ${MQTT_PORT:-1883} + MQTT_PATH: ${MQTT_PATH:-webhook} + # MQTT_USER: ${MQTT_USER} + # MQTT_PASSWORD: ${MQTT_PASSWORD} + # MQTT_TLS: ${MQTT_TLS:-false} + # WEBHOOK_TOKEN: ${WEBHOOK_TOKEN} # example config for https with traefik # networks: # - traefik_proxy labels: - "traefik.enable=true" - "traefik.http.routers.webhook2mqtt_router_http.entrypoints=web" - - "traefik.http.routers.webhook2mqtt_router_http.rule=Host(`netatmo.jru.me`)" + - "traefik.http.routers.webhook2mqtt_router_http.rule=Host(`${WEBHOOK_HOST}`)" # - "traefik.http.middlewares.https_redirect.redirectscheme.scheme=https" # - "traefik.http.routers.webhook2mqtt_router_http.middlewares=https_redirect" - "traefik.http.routers.webhook2mqtt_router.entrypoints=websecure" - - "traefik.http.routers.webhook2mqtt_router.rule=Host(`netatmo.jru.me`)" + - "traefik.http.routers.webhook2mqtt_router.rule=Host(`${WEBHOOK_HOST}`)" - "traefik.http.routers.webhook2mqtt_router.tls=true" - "traefik.http.routers.webhook2mqtt_router.tls.certresolver=my_certresolver" - restart: always # log to syslog | optional logging: driver: syslog diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..71c015d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask==3.1.3 +paho-mqtt==2.1.0 +gunicorn==26.0.0