[Hardening] Bot HTTP server: default bind to 127.0.0.1 and cap /notify request size #111

Closed
opened 2026-07-14 19:50:32 +00:00 by claude-bot · 2 comments
Contributor

Context

The bot runs an internal aiohttp server for incoming calls from the backend (/health, /notify, /record/* — routes registered at bot/questboard_bot/main.py:217-220, served via web.TCPSite(runner, settings.http_host, settings.http_port) at :291).

Its bind address defaults to all interfaces: http_host: str = "0.0.0.0" (bot/questboard_bot/config.py:27). Under Docker Compose this is safe — the bot service uses expose only, no published ports (docker-compose.yml:122-123). But anyone running the bot bare-metal (outside compose) gets an unauthenticated-surface-except-for-X-Bot-Key HTTP server listening on every interface by default. Secure-by-default says loopback.

Additionally, the aiohttp app is created as bare web.Application() (main.py:103) with no explicit client_max_size. Note: aiohttp's default is already 1 MiB, so today's behavior is acceptable — but the bound is implicit and unpinned; make it explicit so a future aiohttp default change or a copy-paste refactor can't silently remove it, and so the intent is documented.

Motivation

Defense in depth for self-hosters: loopback default bind, explicit request-size bound on /notify (main.py:109-126) so a leaked BOT_API_KEY (or the pre-auth request-body read) can't be used for memory abuse with giant JSON payloads.

Fix / Spec

  1. Change the default in bot/questboard_bot/config.py:27 to http_host: str = "127.0.0.1".
  2. IMPORTANT — compose must override it: add HTTP_HOST=0.0.0.0 (matching the settings env prefix used by the bot's config; verify the exact env var name pydantic-settings expects) to the bot service environment: in docker-compose.yml, otherwise backend→bot notifications break because the backend reaches the bot over the Docker network. Verify with the compose stack after the change.
  3. Make the body cap explicit: web.Application(client_max_size=1024**2) at main.py:103, with a short comment.
  4. Document the new default and the compose override in .env.example (bot section) and the relevant docs page (docs/OPERATIONS.md or the bot README section).

Acceptance criteria

  • Compose stack: backend→bot /notify delivery still works end-to-end (verify a notification or /health reachability from the backend container).
  • Bare-metal run with no HTTP_HOST env: server binds 127.0.0.1 (assert via config test or netstat).
  • A /notify POST with a body over 1 MiB is rejected (413) instead of being read into memory.
  • Docs/.env.example updated.

References

  • bot/questboard_bot/config.py:25-28 (bind defaults)
  • bot/questboard_bot/main.py:103 (web.Application()), :109-126 (/notify handler), :217-220 (routes), :289-291 (AppRunner/TCPSite)
  • docker-compose.yml:114-123 (bot service; expose only)

Filed from the July 2026 full-project review.

## Context The bot runs an internal aiohttp server for incoming calls from the backend (`/health`, `/notify`, `/record/*` — routes registered at `bot/questboard_bot/main.py:217-220`, served via `web.TCPSite(runner, settings.http_host, settings.http_port)` at `:291`). Its bind address defaults to all interfaces: `http_host: str = "0.0.0.0"` (`bot/questboard_bot/config.py:27`). Under Docker Compose this is safe — the bot service uses `expose` only, no published ports (`docker-compose.yml:122-123`). But anyone running the bot bare-metal (outside compose) gets an unauthenticated-surface-except-for-X-Bot-Key HTTP server listening on every interface by default. Secure-by-default says loopback. Additionally, the aiohttp app is created as bare `web.Application()` (`main.py:103`) with no explicit `client_max_size`. Note: aiohttp's default is already 1 MiB, so today's behavior is acceptable — but the bound is implicit and unpinned; make it explicit so a future aiohttp default change or a copy-paste refactor can't silently remove it, and so the intent is documented. ## Motivation Defense in depth for self-hosters: loopback default bind, explicit request-size bound on `/notify` (`main.py:109-126`) so a leaked `BOT_API_KEY` (or the pre-auth request-body read) can't be used for memory abuse with giant JSON payloads. ## Fix / Spec 1. Change the default in `bot/questboard_bot/config.py:27` to `http_host: str = "127.0.0.1"`. 2. **IMPORTANT — compose must override it**: add `HTTP_HOST=0.0.0.0` (matching the settings env prefix used by the bot's config; verify the exact env var name pydantic-settings expects) to the bot service `environment:` in `docker-compose.yml`, otherwise backend→bot notifications break because the backend reaches the bot over the Docker network. Verify with the compose stack after the change. 3. Make the body cap explicit: `web.Application(client_max_size=1024**2)` at `main.py:103`, with a short comment. 4. Document the new default and the compose override in `.env.example` (bot section) and the relevant docs page (`docs/OPERATIONS.md` or the bot README section). ## Acceptance criteria - Compose stack: backend→bot `/notify` delivery still works end-to-end (verify a notification or `/health` reachability from the backend container). - Bare-metal run with no `HTTP_HOST` env: server binds 127.0.0.1 (assert via config test or netstat). - A `/notify` POST with a body over 1 MiB is rejected (413) instead of being read into memory. - Docs/.env.example updated. ## References - `bot/questboard_bot/config.py:25-28` (bind defaults) - `bot/questboard_bot/main.py:103` (`web.Application()`), `:109-126` (`/notify` handler), `:217-220` (routes), `:289-291` (AppRunner/TCPSite) - `docker-compose.yml:114-123` (bot service; `expose` only) _Filed from the July 2026 full-project review._
Author
Contributor

Picking this up as part of a v3.3.0 push. Landing on branch hardening/bot together with #83, #85, and #94 (grouped by component to keep the diffs reviewable).

Picking this up as part of a v3.3.0 push. Landing on branch `hardening/bot` together with #83, #85, and #94 (grouped by component to keep the diffs reviewable).
Author
Contributor

Fixed on main (commit 38945bd, merged via cd315d6). http_host default changed to 127.0.0.1 in config.py; docker-compose.yml overrides with HTTP_HOST=0.0.0.0 on the bot service so backend→bot notifications keep working over the Docker network (verified the config uses no env prefix, so HTTP_HOST is the correct var). Body cap made explicit as web.Application(client_max_size=1024**2). .env.example documents the default and the compose override. Tests assert the loopback default with no env var and the explicit client_max_size. Bot suite green (158 passed).

Fixed on `main` (commit `38945bd`, merged via `cd315d6`). `http_host` default changed to `127.0.0.1` in `config.py`; `docker-compose.yml` overrides with `HTTP_HOST=0.0.0.0` on the bot service so backend→bot notifications keep working over the Docker network (verified the config uses no env prefix, so `HTTP_HOST` is the correct var). Body cap made explicit as `web.Application(client_max_size=1024**2)`. `.env.example` documents the default and the compose override. Tests assert the loopback default with no env var and the explicit `client_max_size`. Bot suite green (158 passed).
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#111
No description provided.