From eaa437f923d6fbea2e2e3ea516c3136eda4509e1 Mon Sep 17 00:00:00 2001 From: eric-wien Date: Wed, 1 Jul 2026 22:13:56 +0200 Subject: [PATCH] security: block .env line injection via /model, /fallback, set-persona Telegram text can contain newlines; /model and /fallback wrote their argument into .env verbatim, letting an allow-listed chat inject arbitrary env lines (e.g. CLAUDE_BIN=..., executed on the next restart). Validate model names against a strict pattern, collapse CR/LF in set_env_var() as a backstop, and collapse CR/LF in persona values in persona_set_env() (reached via ogmactl set-persona). Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 13 +++++++++++++ bin/_persona.sh | 3 +++ gateway.py | 14 ++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57b4a31..2ad03eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project aims to follow [Semantic Versioning](https://semver.org/). +## [1.2.2] — 2026-07-01 + +### Security +- **Fixed a `.env` line-injection in `/model`, `/fallback`, and `set-persona` values.** Telegram + messages can contain newlines, and these values were written into `.env` verbatim — so an + allow-listed chat could inject arbitrary env lines (e.g. `CLAUDE_BIN=…`, executed as the gateway + user on the next restart). `/model` and `/fallback` now validate the name against a strict + pattern (letters, digits, `. _ : -`, max 64 chars) before accepting it, `set_env_var()` + unconditionally collapses CR/LF as a backstop, and `persona_set_env()` (used by + `ogmactl set-persona` and `bin/setup`) collapses CR/LF in persona values. Update recommended + for all installs; the risk is bounded by your `TELEGRAM_ALLOWED_USERS` allow-list, but the + allow-list is meant to be a cost/access control, not a shell grant. + ## [1.2.1] — 2026-06-19 ### Fixed diff --git a/bin/_persona.sh b/bin/_persona.sh index 48fbd3e..17315f7 100755 --- a/bin/_persona.sh +++ b/bin/_persona.sh @@ -24,6 +24,9 @@ persona_cfg() { # Pure bash/awk so bin/ogmactl keeps no new dependency. Value may contain spaces/slashes. persona_set_env() { local env="$1" key="$2" val="$3" tmp + # Persona values are single-line by design (persona_render emits one bullet each); + # a line break here would inject arbitrary .env lines, so collapse CR/LF. + val="${val//$'\r'/ }"; val="${val//$'\n'/ }" tmp="$(mktemp)" || return 1 KEY="$key" VAL="$val" awk ' BEGIN { key=ENVIRON["KEY"]; val=ENVIRON["VAL"]; done=0 } diff --git a/gateway.py b/gateway.py index e9508ea..209c686 100755 --- a/gateway.py +++ b/gateway.py @@ -66,6 +66,9 @@ CLAUDE_TIMEOUT = int(os.environ.get("CLAUDE_TIMEOUT", "300")) SESSIONS_FILE = BASE / "sessions.json" ENV_FILE = BASE / ".env" EFFORT_LEVELS = ("low", "medium", "high", "xhigh", "max") +# What a model alias/id may look like (/model, /fallback). Anything outside this — +# especially whitespace/newlines — is refused before it reaches .env or the CLI. +MODEL_NAME_RE = re.compile(r"^[A-Za-z0-9._:-]{1,64}$") # Max concurrent Claude runs. Default 1 — small boxes (e.g. a Pi) OOM if several run at once. MAX_CONCURRENT = max(1, int(cfg("MAX_CONCURRENT", "1") or "1")) @@ -103,6 +106,9 @@ def set_env_var(key: str, value: str) -> None: Lets runtime changes (e.g. /model, /effort) survive a restart. Best-effort. """ + # A line break in the value would inject arbitrary .env lines (e.g. CLAUDE_BIN=…), + # so collapse CR/LF unconditionally — callers validate, this is the backstop. + value = value.replace("\r", " ").replace("\n", " ").strip() try: lines = ENV_FILE.read_text().splitlines() if ENV_FILE.exists() else [] except OSError: @@ -366,6 +372,10 @@ def handle_model(chat_id: str, arg: str) -> None: set_env_var("OGMA_MODEL", "") send(chat_id, "✅ Model reset to the Claude Code default. Applies to your next message.") return + if not MODEL_NAME_RE.match(arg): + send(chat_id, "⚠️ That doesn't look like a model name — use an alias or id " + "(letters, digits, . _ : - only, no spaces).") + return MODEL = arg set_env_var("OGMA_MODEL", arg) send(chat_id, f"✅ Model set to {arg}. Applies to your next message.") @@ -406,6 +416,10 @@ def handle_fallback(chat_id: str, arg: str) -> None: set_env_var("OGMA_FALLBACK_MODEL", "") send(chat_id, "✅ Fallback model cleared.") return + if not MODEL_NAME_RE.match(arg): + send(chat_id, "⚠️ That doesn't look like a model name — use an alias or id " + "(letters, digits, . _ : - only, no spaces).") + return FALLBACK_MODEL = arg set_env_var("OGMA_FALLBACK_MODEL", arg) send(chat_id, f"✅ Fallback model set to {arg}.") -- 2.51.2