security: ensure injected strings are properly escaped

Uses  over  for JSON compatibility.
This commit is contained in:
Jay
2026-08-10 09:46:15 -04:00
parent 622f66e376
commit 6ac95d732f
2 changed files with 79 additions and 29 deletions
+34 -29
View File
@@ -2,6 +2,7 @@ package envelope
import ( import (
"bytes" "bytes"
"encoding/json"
"strconv" "strconv"
) )
@@ -19,13 +20,13 @@ func EncloseEvent(event []byte) Envelope {
// Format: ["OK", eventID, status, message] // Format: ["OK", eventID, status, message]
func EncloseOK(eventID string, status bool, message string) Envelope { func EncloseOK(eventID string, status bool, message string) Envelope {
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString(`["OK","`) buf.WriteString(`["OK",`)
buf.WriteString(eventID) buf.Write(quote(eventID))
buf.WriteString(`",`) buf.WriteString(`,`)
buf.WriteString(strconv.FormatBool(status)) buf.WriteString(strconv.FormatBool(status))
buf.WriteString(`,"`) buf.WriteString(`,`)
buf.WriteString(message) buf.Write(quote(message))
buf.WriteString(`"]`) buf.WriteString(`]`)
return buf.Bytes() return buf.Bytes()
} }
@@ -33,9 +34,8 @@ func EncloseOK(eventID string, status bool, message string) Envelope {
// Format: ["REQ", subID, filter1, filter2, ...] // Format: ["REQ", subID, filter1, filter2, ...]
func EncloseReq(subID string, filters [][]byte) Envelope { func EncloseReq(subID string, filters [][]byte) Envelope {
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString(`["REQ","`) buf.WriteString(`["REQ",`)
buf.WriteString(subID) buf.Write(quote(subID))
buf.WriteString(`"`)
for _, filter := range filters { for _, filter := range filters {
buf.WriteString(`,`) buf.WriteString(`,`)
@@ -50,9 +50,9 @@ func EncloseReq(subID string, filters [][]byte) Envelope {
// Format: ["EVENT", subID, event] // Format: ["EVENT", subID, event]
func EncloseSubscriptionEvent(subID string, event []byte) Envelope { func EncloseSubscriptionEvent(subID string, event []byte) Envelope {
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString(`["EVENT","`) buf.WriteString(`["EVENT",`)
buf.WriteString(subID) buf.Write(quote(subID))
buf.WriteString(`",`) buf.WriteString(`,`)
buf.Write(event) buf.Write(event)
buf.WriteByte(']') buf.WriteByte(']')
return buf.Bytes() return buf.Bytes()
@@ -62,9 +62,9 @@ func EncloseSubscriptionEvent(subID string, event []byte) Envelope {
// Format: ["EOSE", subID] // Format: ["EOSE", subID]
func EncloseEOSE(subID string) Envelope { func EncloseEOSE(subID string) Envelope {
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString(`["EOSE","`) buf.WriteString(`["EOSE",`)
buf.WriteString(subID) buf.Write(quote(subID))
buf.WriteString(`"]`) buf.WriteString(`]`)
return buf.Bytes() return buf.Bytes()
} }
@@ -72,9 +72,9 @@ func EncloseEOSE(subID string) Envelope {
// Format: ["CLOSE", subID] // Format: ["CLOSE", subID]
func EncloseClose(subID string) Envelope { func EncloseClose(subID string) Envelope {
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString(`["CLOSE","`) buf.WriteString(`["CLOSE",`)
buf.WriteString(subID) buf.Write(quote(subID))
buf.WriteString(`"]`) buf.WriteString(`]`)
return buf.Bytes() return buf.Bytes()
} }
@@ -82,11 +82,11 @@ func EncloseClose(subID string) Envelope {
// Format: ["CLOSED", subID, message] // Format: ["CLOSED", subID, message]
func EncloseClosed(subID string, message string) Envelope { func EncloseClosed(subID string, message string) Envelope {
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString(`["CLOSED","`) buf.WriteString(`["CLOSED",`)
buf.WriteString(subID) buf.Write(quote(subID))
buf.WriteString(`","`) buf.WriteString(`,`)
buf.WriteString(message) buf.Write(quote(message))
buf.WriteString(`"]`) buf.WriteString(`]`)
return buf.Bytes() return buf.Bytes()
} }
@@ -94,9 +94,9 @@ func EncloseClosed(subID string, message string) Envelope {
// Format: ["NOTICE", message] // Format: ["NOTICE", message]
func EncloseNotice(message string) Envelope { func EncloseNotice(message string) Envelope {
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString(`["NOTICE","`) buf.WriteString(`["NOTICE",`)
buf.WriteString(message) buf.Write(quote(message))
buf.WriteString(`"]`) buf.WriteString(`]`)
return buf.Bytes() return buf.Bytes()
} }
@@ -104,9 +104,9 @@ func EncloseNotice(message string) Envelope {
// Format: ["AUTH", challenge] // Format: ["AUTH", challenge]
func EncloseAuthChallenge(challenge string) Envelope { func EncloseAuthChallenge(challenge string) Envelope {
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString(`["AUTH","`) buf.WriteString(`["AUTH",`)
buf.WriteString(challenge) buf.Write(quote(challenge))
buf.WriteString(`"]`) buf.WriteString(`]`)
return buf.Bytes() return buf.Bytes()
} }
@@ -119,3 +119,8 @@ func EncloseAuthResponse(event []byte) Envelope {
buf.WriteByte(']') buf.WriteByte(']')
return buf.Bytes() return buf.Bytes()
} }
func quote(s string) []byte {
q, _ := json.Marshal(s)
return q
}
+45
View File
@@ -50,6 +50,13 @@ func TestEncloseOK(t *testing.T) {
message: "Event accepted", message: "Event accepted",
want: []byte(`["OK","abc123",true,"Event accepted"]`), want: []byte(`["OK","abc123",true,"Event accepted"]`),
}, },
{
name: "escapes quotes",
eventID: `"abc123"`,
status: true,
message: `"Event accepted"`,
want: []byte(`["OK","\"abc123\"",true,"\"Event accepted\""]`),
},
{ {
name: "rejected event", name: "rejected event",
eventID: "xyz789", eventID: "xyz789",
@@ -98,6 +105,12 @@ func TestEncloseReq(t *testing.T) {
filters: [][]byte{}, filters: [][]byte{},
want: []byte(`["REQ","sub3"]`), want: []byte(`["REQ","sub3"]`),
}, },
{
name: "escapes quotes",
subID: `"sub3"`,
filters: [][]byte{},
want: []byte(`["REQ","\"sub3\""]`),
},
} }
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@@ -126,6 +139,12 @@ func TestEncloseSubscriptionEvent(t *testing.T) {
event: []byte(`{}`), event: []byte(`{}`),
want: []byte(`["EVENT","sub2",{}]`), want: []byte(`["EVENT","sub2",{}]`),
}, },
{
name: "escapes quotes",
subID: `"sub2"`,
event: []byte(`{}`),
want: []byte(`["EVENT","\"sub2\"",{}]`),
},
} }
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@@ -146,6 +165,11 @@ func TestEncloseEOSE(t *testing.T) {
subID: "sub1", subID: "sub1",
want: []byte(`["EOSE","sub1"]`), want: []byte(`["EOSE","sub1"]`),
}, },
{
name: "escapes quotes",
subID: `"sub1"`,
want: []byte(`["EOSE","\"sub1\""]`),
},
{ {
name: "empty subscription ID", name: "empty subscription ID",
subID: "", subID: "",
@@ -171,6 +195,11 @@ func TestEncloseClose(t *testing.T) {
subID: "sub1", subID: "sub1",
want: []byte(`["CLOSE","sub1"]`), want: []byte(`["CLOSE","sub1"]`),
}, },
{
name: "escapes quotes",
subID: `"sub1"`,
want: []byte(`["CLOSE","\"sub1\""]`),
},
{ {
name: "empty subscription ID", name: "empty subscription ID",
subID: "", subID: "",
@@ -198,6 +227,12 @@ func TestEncloseClosed(t *testing.T) {
message: "Subscription complete", message: "Subscription complete",
want: []byte(`["CLOSED","sub1","Subscription complete"]`), want: []byte(`["CLOSED","sub1","Subscription complete"]`),
}, },
{
name: "escapes quotes",
subID: `"sub1"`,
message: `"Subscription complete"`,
want: []byte(`["CLOSED","\"sub1\"","\"Subscription complete\""]`),
},
{ {
name: "empty message", name: "empty message",
subID: "sub2", subID: "sub2",
@@ -224,6 +259,11 @@ func TestEncloseNotice(t *testing.T) {
message: "This is a notice", message: "This is a notice",
want: []byte(`["NOTICE","This is a notice"]`), want: []byte(`["NOTICE","This is a notice"]`),
}, },
{
name: "escapes quotes",
message: `"This is a notice"`,
want: []byte(`["NOTICE","\"This is a notice\""]`),
},
{ {
name: "empty message", name: "empty message",
message: "", message: "",
@@ -249,6 +289,11 @@ func TestEncloseAuthChallenge(t *testing.T) {
challenge: "random-challenge-string", challenge: "random-challenge-string",
want: []byte(`["AUTH","random-challenge-string"]`), want: []byte(`["AUTH","random-challenge-string"]`),
}, },
{
name: "escapes quotes",
challenge: `"random-challenge-string"`,
want: []byte(`["AUTH","\"random-challenge-string\""]`),
},
{ {
name: "empty challenge", name: "empty challenge",
challenge: "", challenge: "",