mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-09-16 04:45:34 +00:00
Four defects in how configuration is written and displayed. All are
reachable without authentication when web_viewer_password is unset,
which is an explicitly supported setup.
- ini_writer: reject sections, keys and values that cannot survive the
round-trip. update_ini_values() wrote free text straight into the
file, so a newline in any value ended the key's line and everything
after it was re-parsed as INI on the next load. Saving a greeting of
"Welcome!\n[Injected]\npwned = true" through the plugin settings
endpoint created a real new section; a repeated section name bricked
startup with DuplicateSectionError. The check lives at the writer
because three separate callers reach it, and raises IniValueError so a
bad payload is a 400 instead of a half-written file. DEFAULT is
refused as well: its keys apply to every section, and
ConfigParser.add_section('DEFAULT') raises.
- settings_store, web_viewer: persist to disk before mirroring into the
in-memory config. The mirror ran first, so a value rejected by the
writer stayed live in the running process despite never reaching
disk. Both routes taking free-form input (the zombie and offline
alert emails) had the same ordering. The zombie route also caught
only OSError, so an IniValueError there escaped as a 500 rather than
the intended 400.
- config_snapshot: redact Discord webhook URLs. Neither
discord_webhook_urls nor the [DiscordBridge] bridge.<channel> keys
matched any redaction rule, so --show-config and /admin/config
printed live webhook secrets in full; anyone holding one can post to
the channel. Telegram's api_token was already redacted. The
bridge. prefix needs its own rule because the varying part is the
channel name, leaving no fixed stem for the substring match.
- mqtt_weather, packet_capture: verify TLS certificates by default.
Both called tls_set(cert_reqs=ssl.CERT_NONE) unconditionally, so the
broker username and password sent immediately afterwards were
readable by anyone able to intercept the connection.
BREAKING: brokers presenting self-signed certificates now fail to
connect until tls_insecure (mqtt_weather) or mqttN_tls_insecure
(packet_capture) is set to true. Both log a warning while enabled.
56 lines
1.9 KiB
Python
56 lines
1.9 KiB
Python
"""Helpers for rendering resolved config snapshots with redaction."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from configparser import ConfigParser
|
|
|
|
_REDACT_KEY_PARTS: tuple[str, ...] = (
|
|
"password",
|
|
"smtp_password",
|
|
"api_key",
|
|
"token",
|
|
"secret",
|
|
"smtp_user",
|
|
# A Discord webhook URL is a bearer credential: anyone holding it can post
|
|
# to the channel. Covers ``discord_webhook_urls``.
|
|
"webhook",
|
|
)
|
|
|
|
# Keys whose *prefix* marks a secret. [DiscordBridge] maps channels as
|
|
# ``bridge.<meshcore_channel> = <discord webhook url>``, so the varying part is
|
|
# the channel name and there is no fixed stem for the substring rule to catch.
|
|
_REDACT_KEY_PREFIXES: tuple[str, ...] = (
|
|
"bridge.",
|
|
)
|
|
|
|
|
|
def is_sensitive_key(key: str) -> bool:
|
|
"""Return True when a config key should be redacted."""
|
|
key_lower = key.lower()
|
|
if any(key_lower.startswith(prefix) for prefix in _REDACT_KEY_PREFIXES):
|
|
return True
|
|
return any(part in key_lower for part in _REDACT_KEY_PARTS)
|
|
|
|
|
|
def config_to_redacted_sections(config: ConfigParser) -> dict[str, dict[str, str]]:
|
|
"""Return config sections as key/value maps with sensitive keys redacted."""
|
|
sections: dict[str, dict[str, str]] = {}
|
|
for section in config.sections():
|
|
sections[section] = {
|
|
key: "●●●●●●" if is_sensitive_key(key) else value
|
|
for key, value in config.items(section, raw=True)
|
|
}
|
|
return sections
|
|
|
|
|
|
def redacted_sections_to_ini_text(sections: dict[str, dict[str, str]]) -> str:
|
|
"""Render redacted config sections as human-readable INI text."""
|
|
lines: list[str] = []
|
|
for idx, (section_name, options) in enumerate(sections.items()):
|
|
if idx > 0:
|
|
lines.append("")
|
|
lines.append(f"[{section_name}]")
|
|
for key, value in options.items():
|
|
lines.append(f"{key} = {value}")
|
|
return "\n".join(lines)
|