Summary
Someone who can already edit the proxy's config file on the box can put any text, of any length, into one monitoring alert.
Impact
A local principal who already owns the config file can place arbitrary JSON-escaped text of arbitrary length inside the attr.error field of a single Atlas-flagged log record; no log-line forgery, no HTTP header/body framing break, no cross-principal disclosure.
Severity: low
Exploitability: theoretical — Reaching the alert sink requires local write access to the service's own config file plus SIGHUP, at which point the attacker already controls the proxy outright; both emitters JSON-escape the payload so no record forgery or response splitting occurs.
Location
- monguard/src/config/error.rs:70 in impl Display for ConfigError
- monguard/src/config/loader.rs:120 in impl Display for ReloadError
- monguard/src/runtime/reload.rs:146 in ReloadService::handle_reload
- monguard/src/http/validate.rs:149 in validate_config_blob
- monguard/src/config/resolver.rs:890 in validate_listener_references
Reproduction / trigger path
Analyzed trigger path from static analysis — not an executed PoC. Confirm with a concrete repro before handing off.
Attacker controls: Full content and length of any free-text YAML field that the resolver echoes into a ConfigError (e.g. listeners.port[N].upstream, TLS file paths). On the HTTP path the total request body is capped at 1 MiB; on the SIGHUP path the only bound is the on-disk config file size.
- (source) monguard/src/config/loader.rs:60 in reload_config — On SIGHUP, RawConfig::from_file_with_content reads the on-disk YAML config file.
- (hop) monguard/src/config/resolver.rs:890 in validate_listener_references — Raw YAML port.upstream string is cloned verbatim into ConfigError::UnresolvedReference.name.
- (hop) monguard/src/config/error.rs:70 in impl Display for ConfigError — Display writes
{name}
with plain {} formatting — no escaping or truncation.
- (hop) monguard/src/config/loader.rs:120 in impl Display for ReloadError — ValidationFailed arm appends each ConfigError via
{e} into one multi-line message.
# (sink) monguard/src/runtime/reload.rs:146 in ReloadService::handle_reload — tracing::error!(error = %e, shouldIngest=true, shouldAlert=true) emits the string through the logv2 StructuredLogLayer JSON encoder.
h2. Root cause
- `ConfigError` Display arms interpolate config-derived strings with bare `{}` and no length cap or control-char escaping (monguard/src/config/error.rs:48,52,56,60,70).
- `validate_listener_references` clones the raw YAML `port.upstream` string straight into `UnresolvedReference.name` (monguard/src/config/resolver.rs:890); `resolve_required_file` clones the raw YAML path into `FileNotFound.path` (resolver.rs:683).
- `ReloadError` Display concatenates every `ConfigError` via `{e}` into one multi-line string (monguard/src/config/loader.rs:117-122).
- `ReloadService::handle_reload` logs that string as `error = %e` with `shouldIngest=true, shouldAlert=true` (monguard/src/runtime/reload.rs:142-148); the surrounding tracing subscriber is the logv2 StructuredLogLayer JSON encoder (monguard/src/observability/tracing.rs:295-311,374,422), which escapes the value, and the HTTP path serializes via serde_json (monguard/src/http/validate.rs:306).
Filed from Aegis finding e3cf02ade04d (scan scan-3c7821728fe9) · primitive: Attacker-chosen YAML string flows verbatim through error Display into a single JSON-encoded Atlas alert log record. · categories: Log Injection