See group access and ID discovery for trusted/mixed groups and first-install defaults.
This document describes what the plugin actually protects and what it does not. It reflects the available audit notes and regression tests; it does not promise absolute security.
| Parameter | Value |
|---|---|
| Scope | the hermes-max-integration plugin (adapter.py, mixins/, scripts, documentation) |
| Audit baseline | commit b004c573318f1d5e521255531d1943cc8a8b8b90 (main) |
| Findings backlog | BACKLOG.md (audit items SEC-01…SEC-07, CODE-01…CODE-08) |
| Document updated | 2026-10-12; verify release and test counts against the current checkout before relying on them |
| What was checked | static code analysis, isolated tests, pytest regression suite (see current CI for counts) |
| What was NOT checked | production E2E with real users, external platform rules, commit history prior to fork |
Evidence markers (as in the backlog): S — static code/document comparison, R — isolated execution with mocks (not the complete plugin), E — actual local command, V — hypothesis or external contract requiring verification.
Audit status and test counts are historical evidence, not a guarantee that the current branch has been audited. Check the current backlog and CI before deployment.
| Party | Assumed | Limitation |
|---|---|---|
| MAX Bot API | source of message_created, message_callback, bot_started events |
event authenticity is confirmed only by the webhook secret; in long polling it relies on trusting the API |
| User/chat | identifiers come from the event | authorized via allowlists; callback queries protected with strict owner binding (SEC-01) |
| Hermes core | receives text and media and runs tools | the plugin is not a security boundary for the core |
| Network | any attachment URL contained in an event | strict DNS pinning and private IP rejection (SEC-02/SEC-03) |
| Gateway host | trusted | the filesystem, .env and the webhook port are protected by you, not by the plugin |
Setup defaults to private access and preserves an existing allowlist unless the operator explicitly selects allow-all. A closed empty allowlist denies incoming events before media downloads or Core dispatch. Tokens and webhook secrets are resolved through the active Hermes secret scope; a missing scoped secret never falls back to another profile's environment credential.
The bundled Russian Trusted Root CA is added only to MAX API and upload clients. Attachment downloads use the normal system trust context and retain hostname allowlisting, public-IP pinning and redirect restrictions. The plugin does not modify Hermes Core source; media routing is enabled through the public StandaloneSendPolicy registration API, not a local Core patch.
| Setting | Default | Risk | Set it to |
|---|---|---|---|
MAX_ALLOW_ALL_USERS |
false |
— | leave false |
MAX_ALLOWED_USERS |
empty | set your own MAX user_id | |
MAX_CROSS_SESSION |
false |
false (enable only for trusted MAX_CROSS_SESSION_USERS) |
|
MAX_WEBHOOK_SECRET |
empty | mandatory in webhook mode, plus network restrictions | |
MAX_WEBHOOK_HOST |
0.0.0.0 |
127.0.0.1 behind a reverse proxy |
|
MAX_GROUP_POLICY |
allowlist |
allowlist with populated lists or closed |
|
MAX_TABLE_AS_IMAGE |
false |
— | as needed |
MAX_AUTO_INSTALL_PLAYWRIGHT |
false |
true the plugin installs a package and browser from the network at runtime |
keep false unless network installation is controlled |
MAX_WEBHOOK_INSECURE_DEV |
false |
false for production, true for local development only |
# ~/.hermes/.env
MAX_BOT_TOKEN=<bot token>
MAX_ALLOWED_USERS=<your MAX user_id> # mandatory — otherwise access is closed (fail-closed)
MAX_CROSS_SESSION=false # session history from other platforms isolated
MAX_WEBHOOK_SECRET=<long random string> # mandatory in webhook mode
MAX_WEBHOOK_HOST=127.0.0.1 # when a reverse proxy fronts the gatewayWith this configuration exactly one user can reach the bot, no inbound ports are open (long polling), and session history from other platforms is not exposed. Also restrict access to the host itself: the .env holding the token must be owner-readable only.
- Long polling (default) requires no inbound ports — the smallest attack surface. If you do not need a public ingress, keep polling.
- Webhook requires public HTTPS: MAX connects to port 443 only. TLS terminates on a reverse proxy (Caddy/Nginx/Traefik/Cloudflare Tunnel) while the plugin listener binds to
127.0.0.1:8646. - Webhook secret is sent by MAX as the raw value of the
X-Max-Bot-Api-Secretheader (not an HMAC signature); the plugin compares it withsecrets.compare_digest. Without a valid secret, the handler responds with 403 before parsing the request body (SEC-04). - Firewall: port
8646must not be reachable from outside. Allow inbound 443 to the reverse proxy only; deny the rest. - Built-in rate limiting for the webhook is 30 requests / 10 seconds per IP, in process memory: it resets on restart and does not distinguish clients behind a proxy. It guards against bursts, not against a targeted attack.
/healthis served without authentication and only proves the process is up. Readiness to receive traffic is verified via/ready(CODE-05).
| Protection | State | Limitation |
|---|---|---|
| User allowlist | S/R | strict isolation: empty list denies access (fail-closed, SEC-05/SEC-06), verified by tests |
Inbound media URL validation (_validate_download_url) |
S/R | strict DNS pinning (blocks private/loopback/link-local IPs and guards against DNS rebinding, SEC-03) |
| Host allowlist for outbound uploads | S | applies to sending media to MAX (*.max.ru, *.oneme.ru), isolated download client |
| Token isolation during media download | S/R | download client without Authorization; token is not transmitted to external URLs (SEC-02) |
| Constant-time secret comparison | S | secrets.compare_digest, fail-closed check before reading request body (SEC-04) |
| Cache directory permissions | S | audio cache and PNG table directory created with mode=0o700 |
| Log sanitization | S | userinfo and query strings stripped from URLs (_safe_url_for_log), token is never logged |
| Incoming media limits | S/R | limits on attachment size (MAX_INBOUND_MEDIA_MAX_BYTES), total size (MAX_INBOUND_MEDIA_TOTAL_BYTES), timeout (SEC-07) |
| CI checks and tests | E | see current CI for pytest, ruff, bandit and pip-audit status; counts vary by revision |
Full statements and acceptance criteria: BACKLOG.md.
| ID | Resolution in v2.10.0 |
|---|---|
| SEC-01 | Implemented universal callback query authorization bound to owner_user_id, scoped chat and TTL |
| SEC-02 | Isolated download client, bot token is never forwarded to arbitrary attachment URLs |
| SEC-03 | Enforced strict DNS pinning with pre-connect IP address validation against SSRF and DNS rebinding |
| SEC-04 | Webhook operates fail-closed: valid secret mandatory before parsing request body |
| SEC-05 | Access to /sessions and /resume restricted to trusted owners (MAX_CROSS_SESSION=false by default) |
| SEC-06 | Group allowlist semantics transitioned to fail-closed and AND-based combination |
| SEC-07 | Strict limits enforced for incoming attachment size, total media volume, attachment count and timeout |
These audit findings describe changes at the stated revisions. Re-check current implementation before treating them as present-day guarantees.
| Object | Location | What to consider |
|---|---|---|
| Audio cache | $HERMES_HOME/cache/audio/ (~/.hermes/cache/audio/), directory 0o700 |
voice messages stay until removed manually; this is private data |
| PNG tables | ~/.hermes/table_images/ |
render cache whose key includes the engine |
| Message dedup | process memory (5 minutes) | not persistent, resets on restart |
| Sessions | Hermes core (SessionDB) | /sessions isolated from other platforms by default (SEC-05) |
| Logs | Hermes gateway log | URLs are logged without query and userinfo; message content is not redacted |
| Network | MAX API + PyPI (when Playwright auto-install is on) | the plugin makes no network calls other than the MAX API and dependency downloads |
| Date | Commit | Scope | What it proves |
|---|---|---|---|
| 2026-07-17 | e87ee64 |
fixes for 10 vulnerabilities from a "full audit" (adapter.py and related code) | that the issues listed in that report were fixed |
| 2026-07-17 | 6e0f77a, beb4af2 |
STT pipeline, README Security section, CHANGELOG v2.1.1 | accompanying changes |
| 2026-07-18 | 70eb490, d9626b5 |
5 remaining MEDIUM/LOW fixes, nosec B104 annotation for bind-all |
targeted fixes |
| 2026-08-17 | e632014 |
downloads no longer follow redirects, SSRF guard for inbound URLs, warning on an empty secret, 11 tests | current download behaviour |
| 2026-09-15 | abd2f0f |
audit backlog of baseline b004c573: 7 SEC, 8 CODE, 4 BUILD, 10 DOC |
audit backlog recording |
| 2026-09-16 | b0ea511 |
documentation alignment and initial tests (DOC-05, DOC-10) | accurate description of intermediate state |
| 2026-09-29 | ad920e6 |
Release v2.10.0: closed all vulnerabilities SEC-01…SEC-07, implemented CODE-01…CODE-08 | resolved at that revision; not a present-day security guarantee |
| 2026-10-01 | bcbf769 |
Release v2.10.2: Desktop UI sidebar (max-sessions-sidebar), file:// URL handling | desktop integration and stabilization at that revision |
- Neither this document nor past audits guarantee the complete absence of vulnerabilities.
- Security statements apply only to the cited revisions and checked evidence.
- Responsibility for ingress, secrets, firewall, host access, and
.envcontents remains with the deployment owner. - Follow safe configuration practices when deploying in production environments.