Skip to content

AuditLogger.log_event lets caller keyword arguments overwrite envelope fields (run_id, operator, timestamp, prev_hash); the library guide's run_id= example breaks HMAC verification (CR-A07-009) #377

Open

Description

@cemililik

Wave 1 · theme D1-2: Audit chain, integrity manifest and verifier robustness · highest severity High · 3 finding(s) · effort small

Summary

An orchestrator integration follows the guide and logs run_id=; every later verify-audit with the secret exits 6 although nothing was tampered with (or, without a secret, the ForgeLM run_id is overwritten so the chain cannot be correlated). A library caller relaying external values can spoof operator/timestamp attribution in an entry that verifies.

Primary finding

CR-A07-009 — AuditLogger.log_event lets caller keyword arguments overwrite envelope fields (run_id, operator, timestamp, prev_hash); the library guide's run_id= example breaks HMAC verification

severity High · reachability default · intent contract-violation · confidence confirmed · independently confirmed · reproduced end to end

What happens. log_event builds the entry as {timestamp, run_id, operator, event, prev_hash, **details} (forgelm/compliance.py:552-562), so a details key with an envelope name silently replaces the reserved value. The writer signs with the key derived from the logger's own run_id (258), while the verifier derives the key from the entry's run_id (2429). A different run_id= therefore yields a permanent false 'HMAC mismatch' (exit 6). operator= and timestamp= misattribute a line that still verifies, and prev_hash= breaks the chain. docs/guides/library_api.md:111-118 passes run_id=os.environ['RUN_ID'].

Where.

Evidence.

  • compliance.py:552-562 (entry construction, **details at 561). s3_audit.py: D1 run_id 'airflow-run-1' with HMAC -> 'line 5: HMAC mismatch', CLI verify-audit exit 6; D3 operator='someone-else', timestamp='1999-01-01...' -> valid=True; D4 prev_hash='genesis' -> chain broken. docs/guides/library_api.md:93-101.
  • Repro: a guide-pattern logger (auto run_id fg-25a0b3a47929) writes run_id='airflow-run-5678'. Chain-only verify passes. With the secret: 'line 3: HMAC mismatch', classified integrity. CLI forgelm verify-audit exits 6 with the secret and 0 without. Fresh-log override: 'manifest mismatch: pinned run_id 'fg-…' does not match line 1 run_id 'other-run''. operator='someone-else' and timestamp='1999-…' are written and pass require_hmac.
  • scratch/reserved.py: log_event('a.two', operator='spoofed-operator', timestamp='1999-01-01...') is written as given; log_event('a.three', prev_hash='deadbeef') yields verify_audit_log -> valid=False 'chain broken at line 3'.

Recommended fix. Put **details first and the reserved fields after (or reject reserved keys with ValueError); rename the guide's field (e.g. orchestrator_run_id); add a regression test.

Other findings with the same root cause

These close with the same fix; update them in the same pull request.

DOC-D2B04-006 — docs/guides/library_api.md: broken example — Emit your own orchestrator event into the same chain with `logger.log_event("training.completed", orchestrator="airflow", dag_id="train", run_id=os.enviro…

severity High

Where. docs/guides/library_api.md:107-118 (Turkish mirror: docs/guides/library_api-tr.md:108-119) · type: broken example

The document says. Emit your own orchestrator event into the same chain with logger.log_event("training.completed", orchestrator="airflow", dag_id="train", run_id=os.environ["RUN_ID"], outcome="success" if result.success else "reverted").

What the code does. log_event builds the entry as {timestamp, run_id: self.run_id, operator, event, prev_hash, **details}, so the run_id= kwarg silently overwrites the entry's run_id. The HMAC key is derived from the logger's own run_id (sha256(secret + self.run_id)), but the verifier derives the key from the entry's run_id. Result with FORGELM_AUDIT_SECRET set: verify_audit_log(..., hmac_secret=...) -> 'line 2: HMAC mismatch', and forgelm verify-audit prints 'FAIL at line 2: line 2: HMAC mismatch' and exits 6, the tampering code (runtime-confirmed). Without a secret, the entry is attributed to a foreign run_id. Two smaller problems: 'training.completed' is not an event in docs/reference/audit_event_catalog.md, and result.success == False does not imply a revert (TrainResult has a separate reverted field).

Code references. forgelm/compliance.py:555-561, forgelm/compliance.py:258, forgelm/results.py (TrainResult.reverted)

User impact. Following the guide makes a legitimate audit log fail verification permanently with the tamper exit code (6) once HMAC is enabled. That destroys the evidentiary value of the Art. 12 record and raises false security alarms.

Proposed correction. Doc: use a non-reserved key and a namespaced event, e.g. logger.log_event("orchestrator.run_linked", orchestrator="airflow", dag_id="train", orchestrator_run_id=os.environ["RUN_ID"], forgelm_run_id=result.run_id, outcome="reverted" if result.reverted else ("success" if result.success else "failed")). Add a warning that timestamp, run_id, operator, event and prev_hash are reserved. Code: fix log_event so it rejects (ValueError) or namespaces reserved keys in **details.

Turkish mirror. the Turkish page has the same error; fix both in the same PR

DOC-D2B04-018 — docs/guides/library_api.md: misleading — log_event('training.completed', ...) is an event the audit chain accepts

severity Medium

Where. docs/guides/library_api.md:93-101 · type: misleading

The document says. log_event('training.completed', ...) is an event the audit chain accepts

What the code does. training.completed is not in docs/reference/audit_event_catalog.md (only training.started); log_event does not validate names, and the example's run_id= keyword clobbers the envelope run_id.

Code references. docs/reference/audit_event_catalog.md:31, forgelm/compliance.py:561

User impact. Uncatalogued event plus an HMAC-breaking keyword in the recommended pattern.

Proposed correction. Use a catalogued event and a non-reserved key name; fix log_event to protect reserved fields.

Turkish mirror. the Turkish page has the same error; fix both in the same PR

Acceptance criteria

  • A regression test reproduces the failure scenario and asserts the corrected behaviour.
  • Documentation listed above is corrected in English and in its Turkish mirror in the same PR.
  • Every finding in this issue is resolved or explicitly closed with a reason.

Found by the October 2026 ForgeLM code and documentation review against commit cab2947. Code links point at that commit; line numbers may have moved since. Finding IDs (CR-…, DOC-…, TECH-…) are stable references for this backlog.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: complianceCode area: compliancebugSomething isn't workingdocumentationImprovements or additions to documentationsecuritySecurity-relevantseverity: highWrong behaviour or a false claim users rely onsource: review-2026-10Found by the October 2026 code and documentation reviewwave: 1Roadmap wave 1: high severity, small effort

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions