1 Commits
Author SHA1 Message Date
clawbot ca4dadf1eb Refuse a PRIVMSG or NOTICE whose body or meta has the wrong type (closes #245)
check / check (push) Canceled after 0s
Over the HTTP API, a PRIVMSG or NOTICE body was checked only by
decoding it into a list of strings. JSON null decodes into an empty
string there, and meta was not checked at all, so a null line, or a
meta that is not an object, reached every recipient.

handlePrivmsg now answers 400 invalid request body, before anything
else, when body is present and is not an array of strings, or meta is
present and is not an object. A missing or empty body still gets
ERR_NOTEXTTOSEND. README.md says so.

Model: opus-5-5
2026-11-02 05:34:23 +00:00
2 changed files with 31 additions and 41 deletions
+19 -22
View File
@@ -994,7 +994,7 @@ func (hdlr *Handlers) dispatchCommand(
hdlr.handlePrivmsg(
writer, request,
sessionID, clientID, nick,
command, target, body, meta,
command, target, body, meta, bodyLines,
)
case irc.CmdJoin:
hdlr.handleJoin(
@@ -1139,9 +1139,9 @@ func (hdlr *Handlers) handlePrivmsg(
nick, command, target string,
body json.RawMessage,
meta json.RawMessage,
bodyLines func() []string,
) {
lines, ok := checkBodyAndMeta(body, meta)
if !ok {
if !validBodyAndMeta(body, meta) {
hdlr.respondError(
writer, request,
"invalid request body",
@@ -1165,6 +1165,7 @@ func (hdlr *Handlers) handlePrivmsg(
return
}
lines := bodyLines()
if len(lines) == 0 {
hdlr.enqueueNumeric(
request.Context(), clientID,
@@ -1198,29 +1199,25 @@ func (hdlr *Handlers) handlePrivmsg(
)
}
// checkBodyAndMeta returns the lines in body. It returns false
// when body is present and is not an array of strings, or meta
// is present and is not an object. Each line is decoded as any
// because decoding into []string turns a null line into "". A
// null body or meta decodes without an error into nil.
func checkBodyAndMeta(body, meta json.RawMessage) ([]string, bool) {
var lines []string
// validBodyAndMeta reports whether body, when present, is an
// array of strings and meta, when present, is an object.
// Decoding into []string would turn a null line into "", so
// each line is decoded as any and checked. A null body or
// meta decodes without an error into nil.
func validBodyAndMeta(body, meta json.RawMessage) bool {
if body != nil {
var values []any
var lines []any
err := json.Unmarshal(body, &values)
if err != nil || values == nil {
return nil, false
err := json.Unmarshal(body, &lines)
if err != nil || lines == nil {
return false
}
for _, value := range values {
line, isString := value.(string)
for _, line := range lines {
_, isString := line.(string)
if !isString {
return nil, false
return false
}
lines = append(lines, line)
}
}
@@ -1229,11 +1226,11 @@ func checkBodyAndMeta(body, meta json.RawMessage) ([]string, bool) {
err := json.Unmarshal(meta, &fields)
if err != nil || fields == nil {
return nil, false
return false
}
}
return lines, true
return true
}
// respondIRCError enqueues a numeric error reply, notifies
+12 -19
View File
@@ -1001,8 +1001,6 @@ func TestChannelMessage(t *testing.T) {
}
}
// TestMessageMissingBody checks that a PRIVMSG with no body,
// or an empty one, gets ERR_NOTEXTTOSEND.
func TestMessageMissingBody(t *testing.T) {
t.Parallel()
@@ -1017,25 +1015,20 @@ func TestMessageMissingBody(t *testing.T) {
_, lastID := tserver.pollMessages(token, 0)
for _, request := range []map[string]any{
{commandKey: privmsgCmd, toKey: channel},
{commandKey: privmsgCmd, toKey: channel, bodyKey: []string{}},
} {
status, _ := tserver.sendCommand(token, request)
if status != http.StatusOK {
t.Fatalf("%v: expected 200, got %d", request, status)
}
status, _ := tserver.sendCommand(token, map[string]any{
commandKey: privmsgCmd, toKey: channel,
})
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
var msgs []map[string]any
msgs, _ := tserver.pollMessages(token, lastID)
msgs, lastID = tserver.pollMessages(token, lastID)
if !findNumeric(msgs, "412") {
t.Fatalf(
"%v: expected ERR_NOTEXTTOSEND (412), got %v",
request, msgs,
)
}
if !findNumeric(msgs, "412") {
t.Fatalf(
"expected ERR_NOTEXTTOSEND (412), got %v",
msgs,
)
}
}