Bug Description
Values interpolated into Hermes log messages are not stripped of control characters, so an agent, tool, or user supplied value that contains a newline forges a second physical log line. Because hermes logs re-parses each physical line into a record (timestamp, level, component, session), the forged line is surfaced as a genuine record with an attacker-chosen level and component. The repo already neutralizes these characters for file-path log sinks (gateway/platforms/base.py _log_safe_path / _LOG_UNSAFE_CHARS, with a test) but the message-text sinks are unprotected.
Steps to Reproduce
- From the repo root, format a record whose interpolated value carries a newline:
PYTHONPATH=. python - <<'PY'
import logging, io
from agent.redact import RedactingFormatter
buf = io.StringIO()
h = logging.StreamHandler(buf)
h.setFormatter(RedactingFormatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
lg = logging.getLogger("tools.approval"); lg.addHandler(h); lg.setLevel(logging.INFO); lg.propagate = False
cmd = "rm -rf /\n2026-07-09 12:00:00 ERROR gateway.run: operator approved deploy"
lg.warning("Hardline block: %s", cmd[:200])
print(buf.getvalue())
PY
- The output is two physical lines, the second of which parses as a genuine
ERROR gateway.run record:
2026-07-09 10:00:00,001 WARNING tools.approval: Hardline block: rm -rf /
2026-07-09 12:00:00 ERROR gateway.run: operator approved deploy
hermes logs errors --component gateway then surfaces the forged second line as a real gateway error.
Expected Behavior
A single log call produces a single record. Control and line-break characters in the interpolated message are neutralized so a supplied value cannot start a new record, matching what _log_safe_path already does for path sinks.
Actual Behavior
RedactingFormatter.format (agent/redact.py) runs secret redaction but never neutralizes control characters, so an embedded newline reaches the log file verbatim and creates a second parseable record.
Affected Component
Agent Core (logging)
Root Cause Analysis
agent/redact.py RedactingFormatter.format formats and redacts but does not strip control characters from the message.
hermes_cli/logs.py parses per physical line: _TS_RE (^ anchored) and _LEVEL_RE (\s(LEVEL)\s), so any physical line beginning with a timestamp and level is treated as a record.
- Unprotected message-text sinks include
tools/approval.py:2259 (model command), tools/terminal_tool.py:2324 (model command and workdir), cron/scheduler.py:2678 (job name and prompt), gateway/platforms/msgraph_webhook.py:190 (model output), agent/agent_runtime_helpers.py:152 (invalid tool-call arguments). The path-sink guard _log_safe_path (gateway/platforms/base.py) shows the neutralization was intended but was applied only to the 3 path sinks.
Proposed Fix
Neutralize control and line-break characters in the interpolated message centrally in RedactingFormatter.format (every file handler uses it), leaving the appended exception traceback intact so multi-line tracebacks still render. A PR is attached.
Environment
- OS: Windows 11
- Python: 3.11.7
- Hermes: 0.18.2 (reproduced on main at current head)
Bug Description
Values interpolated into Hermes log messages are not stripped of control characters, so an agent, tool, or user supplied value that contains a newline forges a second physical log line. Because
hermes logsre-parses each physical line into a record (timestamp, level, component, session), the forged line is surfaced as a genuine record with an attacker-chosen level and component. The repo already neutralizes these characters for file-path log sinks (gateway/platforms/base.py_log_safe_path/_LOG_UNSAFE_CHARS, with a test) but the message-text sinks are unprotected.Steps to Reproduce
ERROR gateway.runrecord:hermes logs errors --component gatewaythen surfaces the forged second line as a real gateway error.Expected Behavior
A single log call produces a single record. Control and line-break characters in the interpolated message are neutralized so a supplied value cannot start a new record, matching what
_log_safe_pathalready does for path sinks.Actual Behavior
RedactingFormatter.format(agent/redact.py) runs secret redaction but never neutralizes control characters, so an embedded newline reaches the log file verbatim and creates a second parseable record.Affected Component
Agent Core (logging)
Root Cause Analysis
agent/redact.pyRedactingFormatter.formatformats and redacts but does not strip control characters from the message.hermes_cli/logs.pyparses per physical line:_TS_RE(^anchored) and_LEVEL_RE(\s(LEVEL)\s), so any physical line beginning with a timestamp and level is treated as a record.tools/approval.py:2259(model command),tools/terminal_tool.py:2324(model command and workdir),cron/scheduler.py:2678(job name and prompt),gateway/platforms/msgraph_webhook.py:190(model output),agent/agent_runtime_helpers.py:152(invalid tool-call arguments). The path-sink guard_log_safe_path(gateway/platforms/base.py) shows the neutralization was intended but was applied only to the 3 path sinks.Proposed Fix
Neutralize control and line-break characters in the interpolated message centrally in
RedactingFormatter.format(every file handler uses it), leaving the appended exception traceback intact so multi-line tracebacks still render. A PR is attached.Environment