From 6ac95d732f556a6f4051328d16a04eac9aeb7b89 Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 10 Aug 2026 09:46:15 -0400 Subject: [PATCH] security: ensure injected strings are properly escaped Uses over for JSON compatibility. --- enclose.go | 63 ++++++++++++++++++++++++++----------------------- enclose_test.go | 45 +++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 29 deletions(-) diff --git a/enclose.go b/enclose.go index c79149b..0b4b4f6 100644 --- a/enclose.go +++ b/enclose.go @@ -2,6 +2,7 @@ package envelope import ( "bytes" + "encoding/json" "strconv" ) @@ -19,13 +20,13 @@ func EncloseEvent(event []byte) Envelope { // Format: ["OK", eventID, status, message] func EncloseOK(eventID string, status bool, message string) Envelope { var buf bytes.Buffer - buf.WriteString(`["OK","`) - buf.WriteString(eventID) - buf.WriteString(`",`) + buf.WriteString(`["OK",`) + buf.Write(quote(eventID)) + buf.WriteString(`,`) buf.WriteString(strconv.FormatBool(status)) - buf.WriteString(`,"`) - buf.WriteString(message) - buf.WriteString(`"]`) + buf.WriteString(`,`) + buf.Write(quote(message)) + buf.WriteString(`]`) return buf.Bytes() } @@ -33,9 +34,8 @@ func EncloseOK(eventID string, status bool, message string) Envelope { // Format: ["REQ", subID, filter1, filter2, ...] func EncloseReq(subID string, filters [][]byte) Envelope { var buf bytes.Buffer - buf.WriteString(`["REQ","`) - buf.WriteString(subID) - buf.WriteString(`"`) + buf.WriteString(`["REQ",`) + buf.Write(quote(subID)) for _, filter := range filters { buf.WriteString(`,`) @@ -50,9 +50,9 @@ func EncloseReq(subID string, filters [][]byte) Envelope { // Format: ["EVENT", subID, event] func EncloseSubscriptionEvent(subID string, event []byte) Envelope { var buf bytes.Buffer - buf.WriteString(`["EVENT","`) - buf.WriteString(subID) - buf.WriteString(`",`) + buf.WriteString(`["EVENT",`) + buf.Write(quote(subID)) + buf.WriteString(`,`) buf.Write(event) buf.WriteByte(']') return buf.Bytes() @@ -62,9 +62,9 @@ func EncloseSubscriptionEvent(subID string, event []byte) Envelope { // Format: ["EOSE", subID] func EncloseEOSE(subID string) Envelope { var buf bytes.Buffer - buf.WriteString(`["EOSE","`) - buf.WriteString(subID) - buf.WriteString(`"]`) + buf.WriteString(`["EOSE",`) + buf.Write(quote(subID)) + buf.WriteString(`]`) return buf.Bytes() } @@ -72,9 +72,9 @@ func EncloseEOSE(subID string) Envelope { // Format: ["CLOSE", subID] func EncloseClose(subID string) Envelope { var buf bytes.Buffer - buf.WriteString(`["CLOSE","`) - buf.WriteString(subID) - buf.WriteString(`"]`) + buf.WriteString(`["CLOSE",`) + buf.Write(quote(subID)) + buf.WriteString(`]`) return buf.Bytes() } @@ -82,11 +82,11 @@ func EncloseClose(subID string) Envelope { // Format: ["CLOSED", subID, message] func EncloseClosed(subID string, message string) Envelope { var buf bytes.Buffer - buf.WriteString(`["CLOSED","`) - buf.WriteString(subID) - buf.WriteString(`","`) - buf.WriteString(message) - buf.WriteString(`"]`) + buf.WriteString(`["CLOSED",`) + buf.Write(quote(subID)) + buf.WriteString(`,`) + buf.Write(quote(message)) + buf.WriteString(`]`) return buf.Bytes() } @@ -94,9 +94,9 @@ func EncloseClosed(subID string, message string) Envelope { // Format: ["NOTICE", message] func EncloseNotice(message string) Envelope { var buf bytes.Buffer - buf.WriteString(`["NOTICE","`) - buf.WriteString(message) - buf.WriteString(`"]`) + buf.WriteString(`["NOTICE",`) + buf.Write(quote(message)) + buf.WriteString(`]`) return buf.Bytes() } @@ -104,9 +104,9 @@ func EncloseNotice(message string) Envelope { // Format: ["AUTH", challenge] func EncloseAuthChallenge(challenge string) Envelope { var buf bytes.Buffer - buf.WriteString(`["AUTH","`) - buf.WriteString(challenge) - buf.WriteString(`"]`) + buf.WriteString(`["AUTH",`) + buf.Write(quote(challenge)) + buf.WriteString(`]`) return buf.Bytes() } @@ -119,3 +119,8 @@ func EncloseAuthResponse(event []byte) Envelope { buf.WriteByte(']') return buf.Bytes() } + +func quote(s string) []byte { + q, _ := json.Marshal(s) + return q +} diff --git a/enclose_test.go b/enclose_test.go index 8e70a3f..7ea228a 100644 --- a/enclose_test.go +++ b/enclose_test.go @@ -50,6 +50,13 @@ func TestEncloseOK(t *testing.T) { message: "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", eventID: "xyz789", @@ -98,6 +105,12 @@ func TestEncloseReq(t *testing.T) { filters: [][]byte{}, want: []byte(`["REQ","sub3"]`), }, + { + name: "escapes quotes", + subID: `"sub3"`, + filters: [][]byte{}, + want: []byte(`["REQ","\"sub3\""]`), + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -126,6 +139,12 @@ func TestEncloseSubscriptionEvent(t *testing.T) { event: []byte(`{}`), want: []byte(`["EVENT","sub2",{}]`), }, + { + name: "escapes quotes", + subID: `"sub2"`, + event: []byte(`{}`), + want: []byte(`["EVENT","\"sub2\"",{}]`), + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -146,6 +165,11 @@ func TestEncloseEOSE(t *testing.T) { subID: "sub1", want: []byte(`["EOSE","sub1"]`), }, + { + name: "escapes quotes", + subID: `"sub1"`, + want: []byte(`["EOSE","\"sub1\""]`), + }, { name: "empty subscription ID", subID: "", @@ -171,6 +195,11 @@ func TestEncloseClose(t *testing.T) { subID: "sub1", want: []byte(`["CLOSE","sub1"]`), }, + { + name: "escapes quotes", + subID: `"sub1"`, + want: []byte(`["CLOSE","\"sub1\""]`), + }, { name: "empty subscription ID", subID: "", @@ -198,6 +227,12 @@ func TestEncloseClosed(t *testing.T) { message: "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", subID: "sub2", @@ -224,6 +259,11 @@ func TestEncloseNotice(t *testing.T) { message: "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", message: "", @@ -249,6 +289,11 @@ func TestEncloseAuthChallenge(t *testing.T) { challenge: "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", challenge: "",