chat: enforce shared text policy at UI, sign point, and gossip ingress
CI / check (push) Successful in 3m4s
CI / check (push) Successful in 3m4s
Chat-hardening plan Phase 1. The chat body policy (2,000-char + 8 KiB ceilings, single-pass control/whitespace normalization) moves from the UI layer into src/sanitize.rs and is now enforced at every trust boundary: cap_chat_input bounds the live input (oversized paste), the gossip sign point re-sanitizes so non-UI callers can't bypass policy, and gossip ingress rejects oversized raw text before sanitizing (admit_chat_text) and drops messages with neither visible text nor an attachment. The incoming chat author label now uses the strict name sanitizer until Phase 2 roster-binds it. +8 tests (532 lib green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+29
-15
@@ -1,6 +1,8 @@
|
||||
# Chat hardening — ephemeral implementation plan
|
||||
|
||||
**Status (2026-07-15):** PLANNED, not started. This is a temporary scope
|
||||
**Status (2026-07-17):** Phase 1 COMPLETE (shared text policy in `src/sanitize.rs`,
|
||||
ceilings enforced at UI input, sign point, and gossip ingress; all gates green).
|
||||
Phases 2–5 not started. This is a temporary scope
|
||||
contract for hardening the existing room chat. Update the checkboxes and decision
|
||||
log as work lands, then delete this file when the work is complete. Do not add
|
||||
link previews as part of this effort.
|
||||
@@ -88,39 +90,39 @@ tests. Values are starting points, not a compatibility contract.
|
||||
**Target:** downstream layers never receive or retain an unexpectedly large or
|
||||
unsafe chat string.
|
||||
|
||||
- [ ] Move chat constants and `sanitize_chat` from `src/app/mod.rs` into
|
||||
- [x] Move chat constants and `sanitize_chat` from `src/app/mod.rs` into
|
||||
`src/sanitize.rs` (or a narrowly scoped shared chat-policy module if that keeps
|
||||
the API clearer).
|
||||
- [ ] Implement a single-pass sanitizer that:
|
||||
- [x] Implement a single-pass sanitizer that:
|
||||
- maps control characters to spaces;
|
||||
- collapses whitespace and trims ends;
|
||||
- enforces both the character and UTF-8 byte ceilings without splitting a scalar;
|
||||
- returns empty for content with no visible text.
|
||||
- [ ] Add `cap_chat_input` for live editing. It must preserve the user's current
|
||||
- [x] Add `cap_chat_input` for live editing. It must preserve the user's current
|
||||
whitespace while enforcing character and byte ceilings; normalization remains a
|
||||
submit/ingress operation so typing does not visibly jump.
|
||||
- [ ] Apply `cap_chat_input` in `AppMessage::ChatInputChanged`, covering keyboard,
|
||||
- [x] Apply `cap_chat_input` in `AppMessage::ChatInputChanged`, covering keyboard,
|
||||
clipboard, primary-selection, and context-menu paste paths through the controlled
|
||||
input widget.
|
||||
- [ ] Sanitize outgoing text immediately before local echo and `CoreCommand` send.
|
||||
- [ ] Sanitize again before `GossipMessage::Chat` is signed, so a future non-UI
|
||||
- [x] Sanitize outgoing text immediately before local echo and `CoreCommand` send.
|
||||
- [x] Sanitize again before `GossipMessage::Chat` is signed, so a future non-UI
|
||||
caller cannot bypass policy.
|
||||
- [ ] At gossip ingress, reject raw chat text over the byte ceiling before doing
|
||||
- [x] At gossip ingress, reject raw chat text over the byte ceiling before doing
|
||||
downstream sanitization; sanitize accepted text before creating `RoomEvent`.
|
||||
- [ ] Keep attachment-only messages when the sanitized caption is empty; drop a
|
||||
- [x] Keep attachment-only messages when the sanitized caption is empty; drop a
|
||||
chat with neither visible text nor a valid attachment.
|
||||
- [ ] Stop sanitizing an incoming chat `name` with the body sanitizer. Phase 2
|
||||
- [x] Stop sanitizing an incoming chat `name` with the body sanitizer. Phase 2
|
||||
replaces it with the roster-bound name.
|
||||
|
||||
### Phase 1 tests
|
||||
|
||||
- [ ] ASCII, multibyte Unicode, emoji, whitespace, NUL/CR/LF/TAB/ESC, empty input.
|
||||
- [ ] Exact character and byte boundaries, including a four-byte scalar at the
|
||||
- [x] ASCII, multibyte Unicode, emoji, whitespace, NUL/CR/LF/TAB/ESC, empty input.
|
||||
- [x] Exact character and byte boundaries, including a four-byte scalar at the
|
||||
cutoff.
|
||||
- [ ] Oversized paste never makes `state.chat_input` exceed either ceiling.
|
||||
- [ ] Outgoing, incoming, and direct core/network paths converge on the same
|
||||
- [x] Oversized paste never makes `state.chat_input` exceed either ceiling.
|
||||
- [x] Outgoing, incoming, and direct core/network paths converge on the same
|
||||
normalized result.
|
||||
- [ ] Empty captions are retained only when a valid attachment remains.
|
||||
- [x] Empty captions are retained only when a valid attachment remains.
|
||||
|
||||
## Phase 2 — Admission, identity binding, replay, and spam control
|
||||
|
||||
@@ -383,3 +385,15 @@ The plan is complete when:
|
||||
privacy-oriented design.
|
||||
- **2026-07-15:** Initial scope keeps all wire formats stable; hardening is local
|
||||
admission, validation, resource accounting, and honest UI state.
|
||||
- **2026-07-17 (Phase 1):** The 8 KiB byte ceiling deliberately cannot bind on
|
||||
*sanitized* output (2,000 scalars × 4 bytes = 8,000 ≤ 8,192), so inside
|
||||
`sanitize_chat`/`cap_chat_input` it is defense in depth; its operative role is
|
||||
the raw-ingress reject in `admit_chat_text`.
|
||||
- **2026-07-17 (Phase 1):** Interim until Phase 2's roster binding: the incoming
|
||||
chat `name` now goes through the strict `sanitize_name` label sanitizer at the
|
||||
UI edge (was the body sanitizer), so author labels already get bidi/zero-width
|
||||
stripping and the 48-char label cap.
|
||||
- **2026-07-17 (Phase 1):** `send_chat` at the gossip sign point silently no-ops
|
||||
(Ok) on an empty-after-sanitize body with no attachment rather than erroring;
|
||||
the UI already prevents this case, and Phase 5's send-status work is where
|
||||
send-path feedback gets designed.
|
||||
|
||||
Reference in New Issue
Block a user