The HTTP API accepts a nick only if it matches validNickRe (internal/handlers/api.go): a letter or _, then up to 31 of letters, digits and _-[]\^{}|`. The IRC listener takes msg.Params[0] as the nick, cuts it to 32 bytes and checks nothing else, both at registration (handlePreRegistration in internal/ircserver/conn.go) and on a later NICK (handleNick in internal/ircserver/commands.go). README.md (Nick Semantics) records this gap.
A trailing parameter can hold spaces, so NICK :bob PRIVMSG #inj :hi registers the 20-byte nick bob PRIVMSG #inj :hi. Every line the relay then builds from that nick is read by other IRC clients as something else. Seen on eceb5f2 with two IRC listener clients, bob and the attacker, both on #inj:
bob's client receives :bob PRIVMSG #inj :hi!bob PRIVMSG #inj :hi@* JOIN #inj when the attacker joins, and :bob PRIVMSG #inj :hi!bob PRIVMSG #inj :hi@* PRIVMSG #inj :real text when the attacker speaks. An IRC client parses both as a PRIVMSG from bob to #inj: the attacker speaks as bob, with bob present.
NAMES #inj gives bob :neoirc 353 bob = #inj :@bob bob PRIVMSG #inj :hi, which lists bob twice and three names that do not exist.
NICK : after registration sets the nick to the empty string (WHOIS of the old nick then gets 401). Nicks containing !, @, # or a leading : are accepted too and break the nick!user@host form that bans match against, and the 32-byte cut can split a multi-byte character.
Fix: one nick rule in internal/service (next to validChannelRe), used by the HTTP API and by both IRC NICK paths; the IRC listener answers a nick the rule refuses with 432 ERR_ERRONEUSNICKNAME and changes nothing, instead of cutting it. README.md Nick Semantics then says both transports apply the same rule.
Definition of done
Over the IRC listener, NICK before and after registration refuses a nick the HTTP API would refuse (a space, an empty nick, a 33-character nick, !, @, #) with 432, and the nick stays as it was.
The rule exists once, in internal/service, and the HTTP API uses that copy.
Tests in internal/ircserver for the cases in item 1, including that a second client on the channel sees no line from the refused nick.
README.md Nick Semantics no longer says the listener checks nothing else.
One reviewed PR against next.
Model: fable-5-1
The HTTP API accepts a nick only if it matches `validNickRe` (`internal/handlers/api.go`): a letter or `_`, then up to 31 of letters, digits and `` _-[]\^{}|` ``. The IRC listener takes `msg.Params[0]` as the nick, cuts it to 32 bytes and checks nothing else, both at registration (`handlePreRegistration` in `internal/ircserver/conn.go`) and on a later `NICK` (`handleNick` in `internal/ircserver/commands.go`). `README.md` (Nick Semantics) records this gap.
A trailing parameter can hold spaces, so `NICK :bob PRIVMSG #inj :hi` registers the 20-byte nick `bob PRIVMSG #inj :hi`. Every line the relay then builds from that nick is read by other IRC clients as something else. Seen on `eceb5f2` with two IRC listener clients, `bob` and the attacker, both on `#inj`:
- bob's client receives `:bob PRIVMSG #inj :hi!bob PRIVMSG #inj :hi@* JOIN #inj` when the attacker joins, and `:bob PRIVMSG #inj :hi!bob PRIVMSG #inj :hi@* PRIVMSG #inj :real text` when the attacker speaks. An IRC client parses both as a PRIVMSG from `bob` to `#inj`: the attacker speaks as bob, with bob present.
- `NAMES #inj` gives bob `:neoirc 353 bob = #inj :@bob bob PRIVMSG #inj :hi`, which lists `bob` twice and three names that do not exist.
- `NICK :` after registration sets the nick to the empty string (`WHOIS` of the old nick then gets 401). Nicks containing `!`, `@`, `#` or a leading `:` are accepted too and break the `nick!user@host` form that bans match against, and the 32-byte cut can split a multi-byte character.
Fix: one nick rule in `internal/service` (next to `validChannelRe`), used by the HTTP API and by both IRC `NICK` paths; the IRC listener answers a nick the rule refuses with 432 `ERR_ERRONEUSNICKNAME` and changes nothing, instead of cutting it. `README.md` Nick Semantics then says both transports apply the same rule.
## Definition of done
1. Over the IRC listener, `NICK` before and after registration refuses a nick the HTTP API would refuse (a space, an empty nick, a 33-character nick, `!`, `@`, `#`) with 432, and the nick stays as it was.
2. The rule exists once, in `internal/service`, and the HTTP API uses that copy.
3. Tests in `internal/ircserver` for the cases in item 1, including that a second client on the channel sees no line from the refused nick.
4. `README.md` Nick Semantics no longer says the listener checks nothing else.
5. One reviewed PR against `next`.
Model: fable-5-1
clawbot
self-assigned this 2026-10-20 16:51:32 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The HTTP API accepts a nick only if it matches
validNickRe(internal/handlers/api.go): a letter or_, then up to 31 of letters, digits and_-[]\^{}|`. The IRC listener takesmsg.Params[0]as the nick, cuts it to 32 bytes and checks nothing else, both at registration (handlePreRegistrationininternal/ircserver/conn.go) and on a laterNICK(handleNickininternal/ircserver/commands.go).README.md(Nick Semantics) records this gap.A trailing parameter can hold spaces, so
NICK :bob PRIVMSG #inj :hiregisters the 20-byte nickbob PRIVMSG #inj :hi. Every line the relay then builds from that nick is read by other IRC clients as something else. Seen oneceb5f2with two IRC listener clients,boband the attacker, both on#inj::bob PRIVMSG #inj :hi!bob PRIVMSG #inj :hi@* JOIN #injwhen the attacker joins, and:bob PRIVMSG #inj :hi!bob PRIVMSG #inj :hi@* PRIVMSG #inj :real textwhen the attacker speaks. An IRC client parses both as a PRIVMSG frombobto#inj: the attacker speaks as bob, with bob present.NAMES #injgives bob:neoirc 353 bob = #inj :@bob bob PRIVMSG #inj :hi, which listsbobtwice and three names that do not exist.NICK :after registration sets the nick to the empty string (WHOISof the old nick then gets 401). Nicks containing!,@,#or a leading:are accepted too and break thenick!user@hostform that bans match against, and the 32-byte cut can split a multi-byte character.Fix: one nick rule in
internal/service(next tovalidChannelRe), used by the HTTP API and by both IRCNICKpaths; the IRC listener answers a nick the rule refuses with 432ERR_ERRONEUSNICKNAMEand changes nothing, instead of cutting it.README.mdNick Semantics then says both transports apply the same rule.Definition of done
NICKbefore and after registration refuses a nick the HTTP API would refuse (a space, an empty nick, a 33-character nick,!,@,#) with 432, and the nick stays as it was.internal/service, and the HTTP API uses that copy.internal/ircserverfor the cases in item 1, including that a second client on the channel sees no line from the refused nick.README.mdNick Semantics no longer says the listener checks nothing else.next.Model: fable-5-1