1 Commits
Author SHA1 Message Date
clawbot 98a3bf16cd 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. The check returns the lines it accepted,
and the empty-body check counts those, so body is decoded once. A
missing or empty body still gets ERR_NOTEXTTOSEND. README.md says so.

Model: opus-5-5
2026-11-02 06:07:04 +00:00
2 changed files with 42 additions and 32 deletions
+23 -20
View File
@@ -994,7 +994,7 @@ func (hdlr *Handlers) dispatchCommand(
hdlr.handlePrivmsg(
writer, request,
sessionID, clientID, nick,
command, target, body, meta, bodyLines,
command, target, body, meta,
)
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,
) {
if !validBodyAndMeta(body, meta) {
lines, ok := checkBodyAndMeta(body, meta)
if !ok {
hdlr.respondError(
writer, request,
"invalid request body",
@@ -1165,7 +1165,6 @@ func (hdlr *Handlers) handlePrivmsg(
return
}
lines := bodyLines()
if len(lines) == 0 {
hdlr.enqueueNumeric(
request.Context(), clientID,
@@ -1199,25 +1198,29 @@ func (hdlr *Handlers) handlePrivmsg(
)
}
// 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 lines []any
// 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
err := json.Unmarshal(body, &lines)
if err != nil || lines == nil {
return false
if body != nil {
var values []any
err := json.Unmarshal(body, &values)
if err != nil || values == nil {
return nil, false
}
for _, line := range lines {
_, isString := line.(string)
for _, value := range values {
line, isString := value.(string)
if !isString {
return false
return nil, false
}
lines = append(lines, line)
}
}
@@ -1226,11 +1229,11 @@ func validBodyAndMeta(body, meta json.RawMessage) bool {
err := json.Unmarshal(meta, &fields)
if err != nil || fields == nil {
return false
return nil, false
}
}
return true
return lines, true
}
// respondIRCError enqueues a numeric error reply, notifies
+19 -12
View File
@@ -1001,6 +1001,8 @@ 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()
@@ -1015,20 +1017,25 @@ func TestMessageMissingBody(t *testing.T) {
_, lastID := tserver.pollMessages(token, 0)
status, _ := tserver.sendCommand(token, map[string]any{
commandKey: privmsgCmd, toKey: channel,
})
if status != http.StatusOK {
t.Fatalf("expected 200, got %d", status)
}
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)
}
msgs, _ := tserver.pollMessages(token, lastID)
var msgs []map[string]any
if !findNumeric(msgs, "412") {
t.Fatalf(
"expected ERR_NOTEXTTOSEND (412), got %v",
msgs,
)
msgs, lastID = tserver.pollMessages(token, lastID)
if !findNumeric(msgs, "412") {
t.Fatalf(
"%v: expected ERR_NOTEXTTOSEND (412), got %v",
request, msgs,
)
}
}
}