πŸ”„ Topic

Treating a reply to a real person as a short-lived, one-use authorizationβ€”not as something a local process can invent because it can reach an API.


🎯 Goal

Make the final message-delivery boundary prove that an outgoing reply has a real and recent inbound cause.


πŸ›  What I Did

I hardened the local Hermes WhatsApp bridge with an in-memory reply-grant store. When the bridge observes an inbound message, it creates one grant tied to both the inbound message ID and its chat ID. A later text reply must present that exact ID and destination before it can leave the bridge.

The grant has four important properties:

  • it is bound to one chat, so it cannot be replayed to a different person;
  • it is consumed once, so a retry cannot send the same answer twice;
  • it expires after five minutes;
  • it disappears after a bridge restart, because dropping a stale reply is safer than sending it late.

The check happens after content filters have had a chance to reject unsafe output. That detail matters: an output blocked for a different reason must not consume the genuine reply permission that a corrected response still needs.

Focused tests covered a valid one-time reply, a cross-chat mismatch, an expired grant, and an invented message ID.


πŸ”— Key Cybersecurity Connections

This is capability-based authorization in a small, practical form. Access to localhost was not treated as proof of intent, because wrappers, retries, and local processes can all reach the same endpoint. The authorization was narrowed to one action, one conversation, and one short time window.


πŸ” Investigation Questions

  • What event authorizes an outbound message?
  • Can one authorization be replayed?
  • Can a valid reply token be used for another chat?
  • What should happen after a restart?
  • Does an earlier content rejection accidentally burn the permission?

🚨 Detection Opportunities

Useful signals include:

  • missing-inbound-reply-grant
  • reply-grant-chat-mismatch
  • attempted use of an expired grant
  • repeated blocked sends from one local caller

Example triage note:

event=outgoing_message_blocked
reason=missing-inbound-reply-grant
action=inspect_local_caller_and_confirm_no_unsolicited_delivery

🧭 MITRE ATT&CK Techniques

This is mainly defensive design rather than an ATT&CK detection. The closest relevant areas are:

  • T1078 β€” Valid Accounts, where a technically valid path must not become unlimited authority
  • T1059 β€” Command and Scripting Interpreter, because local wrappers should not gain silent delivery power

πŸ—Ί Visual Investigation Diagram

Inbound message
    ↓
Exact one-use reply grant
    ↓
Content safety checks
    ↓
Match message ID + chat ID + time window
    ↓
Send once or block

⚠ Challenges

The tempting shortcut was to trust any caller on the local machine. That is convenient, but it blurs the line between β€œthis process can call the endpoint” and β€œa person just gave this specific reply a reason to exist.”


πŸ“š What I Learned

I learned that context should be carried as data, not left as an instruction for a model or a caller to remember. A reply reference, chat binding, expiry, and one-time consumption are much stronger than β€œonly answer when appropriate.”


➑ Next Steps

  • Keep testing expiration and restart behavior with fixtures
  • Review any new delivery path against the same final-boundary rule
  • Preserve narrow, explicit exceptions instead of broad bypasses

🧠 Reflection

The best part of this change is that it makes the safe outcome boring: a stale or invented send simply does not leave the bridge.


🧩 Lessons Learned

What worked

Binding permission to the exact inbound event and destination.

What broke

The older design assumed a local endpoint was a trusted endpoint.

Why it broke

Local reachability is a network property, not an authorization decision.

Fix / takeaway

Make outbound delivery consume a small, verifiable capability.


πŸ“ˆ Skill Progression Context

This moved me from discussing least privilege in theory to implementing a concrete authorization boundary around a person-facing automation path.


πŸ˜„ TL;DR

A real reply now needs a real, recent inbound message behind itβ€”and can only be sent once.