Skip to content

history: \x05/\x06 record sentinels are not escaped — clients can inject message tags into chathistory replay #103

Description

@rubinlinux

Summary

history.c frames stored records with \x05 and \x06 as in-band sentinels, on the stated assumption that those bytes "neither occur in normal IRC payloads or in channel/nick names" (ircd/history.c:163). Nothing enforces that assumption on the default configuration, so a client can put the sentinels in its own message body and have the deserialiser promote attacker-controlled text into the message-tag prefix of a replayed message.

Reproduced against a stock ircv3.2-upgrade build over plain TCP. No WebSocket, no FEAT_UTF8ONLY, no special privileges.

Reproduction

Client A (any ordinary IRC client):

PRIVMSG #seance :\x06+evil/injected=owned\x06innocent looking text

Client B, with message-tags + draft/chathistory, then CHATHISTORY LATEST #seance * 5, receives:

@batch=hist44AAS;time=2026-08-28T18:34:34.952Z;msgid=ABAAAAAaBDmsja;+evil/injected=owned :victimA!victimA@172.17.0.1 PRIVMSG #seance :innocent looking text

+evil/injected=owned is not a tag client A sent — it is body text that the storage layer re-interpreted as a tag section. The visible message body is clean, so nothing looks wrong to a reader.

Mechanism

  • serialize_message() (ircd/history.c:152-200) writes [\x05<original_target>\x05][\x06<client_tags>\x06]<content> and does not escape or reject those bytes appearing inside content.
  • deserialize_message() (ircd/history.c:287-306) unconditionally lifts a leading \x06…\x06 span out of the content field into msg->client_tags.
  • m_chathistory.c:654 splices that verbatim into the outgoing tag prefix: ircd_snprintf(0, ctags_str, sizeof(ctags_str), ";%s", msg->client_tags), and :638 does the same for TAGMSG.

Nothing sanitises the content on the way in: check_utf8_text() (ircd/ircd_relay.c:104) returns early when FEAT_UTF8ONLY is off, which is the default (ircd_features.c:1006).

Related sentinels

The same class applies to the other in-band markers, which I have not built PoCs for:

  • \x05 forges original_target on PM-pair-keyed records (ircd/replay.c:325), which is what identifies the actual recipient for a nick1:nick2 key.
  • \x1F in replayed content is treated as a draft/multiline separator (m_chathistory.c:658) and reaches the paste listener's HTTP view (paste_listener.c:832).
  • \x1E + ml is read as a multiline content reference (ml_content.c:301).

Worth a look while in there: history.c:2171-2186 matches the +afternet.org/sid= PM-history authorisation token with an unanchored strstr over client_tags. I could not construct a clean bypass by reading alone — a forged section only lands in client_tags if no server-injected sid section and no original_target section precedes it, and PM records normally carry one — so this is flagged for analysis, not claimed as exploitable.

Suggested fix

Escape or reject \x05, \x06, \x1E and \x1F in serialize_message()'s content and client_tags fields rather than relying on them being absent, and anchor the sid match to a ;-delimited token.

Relationship to #102

Found while red-teaming #102. That PR is not the cause — the reproduction above is against a stock build — but the two interact and it seemed worth saying so explicitly. WebSocket clients on the text.ircv3.net subprotocol were accidentally shielded from this, because the UTF-8 sanitiser rewrote every one of these control bytes to U+FFFD before the line reached the parser. #102 fixes that sanitiser (those bytes are valid UTF-8 and it was destroying all IRC formatting and CTCP), which removes the accidental shield and puts WebSocket clients on the same footing as every other client. The right fix is escaping here, not a knowingly-wrong UTF-8 validator elsewhere.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions