Skip to content

Commit 3a89e6c

Browse files
author
Divjot Arora
committed
GODRIVER-1923 Error if BSON cstrings contain null bytes (#622)
1 parent 1a2534c commit 3a89e6c

File tree

4 files changed

+114
-6
lines changed

4 files changed

+114
-6
lines changed

bson/bsonrw/value_writer.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"io"
1313
"math"
1414
"strconv"
15+
"strings"
1516
"sync"
1617

1718
"go.mongodb.org/mongo-driver/bson/bsontype"
@@ -247,7 +248,12 @@ func (vw *valueWriter) invalidTransitionError(destination mode, name string, mod
247248
func (vw *valueWriter) writeElementHeader(t bsontype.Type, destination mode, callerName string, addmodes ...mode) error {
248249
switch vw.stack[vw.frame].mode {
249250
case mElement:
250-
vw.buf = bsoncore.AppendHeader(vw.buf, t, vw.stack[vw.frame].key)
251+
key := vw.stack[vw.frame].key
252+
if !isValidCString(key) {
253+
return errors.New("BSON element key cannot contain null bytes")
254+
}
255+
256+
vw.buf = bsoncore.AppendHeader(vw.buf, t, key)
251257
case mValue:
252258
// TODO: Do this with a cache of the first 1000 or so array keys.
253259
vw.buf = bsoncore.AppendHeader(vw.buf, t, strconv.Itoa(vw.stack[vw.frame].arrkey))
@@ -430,6 +436,9 @@ func (vw *valueWriter) WriteObjectID(oid primitive.ObjectID) error {
430436
}
431437

432438
func (vw *valueWriter) WriteRegex(pattern string, options string) error {
439+
if !isValidCString(pattern) || !isValidCString(options) {
440+
return errors.New("BSON regex values cannot contain null bytes")
441+
}
433442
if err := vw.writeElementHeader(bsontype.Regex, mode(0), "WriteRegex"); err != nil {
434443
return err
435444
}
@@ -602,3 +611,7 @@ func (vw *valueWriter) writeLength() error {
602611
vw.buf[start+3] = byte(length >> 24)
603612
return nil
604613
}
614+
615+
func isValidCString(cs string) bool {
616+
return !strings.ContainsRune(cs, '\x00')
617+
}

bson/marshal_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package bson
88

99
import (
1010
"bytes"
11+
"errors"
1112
"fmt"
1213
"reflect"
1314
"testing"
@@ -267,3 +268,35 @@ func TestCachingEncodersNotSharedAcrossRegistries(t *testing.T) {
267268
})
268269
})
269270
}
271+
272+
func TestNullBytes(t *testing.T) {
273+
t.Run("element keys", func(t *testing.T) {
274+
doc := D{{"a\x00", "foobar"}}
275+
res, err := Marshal(doc)
276+
want := errors.New("BSON element key cannot contain null bytes")
277+
assert.Equal(t, want, err, "expected Marshal error %v, got error %v with result %q", want, err, Raw(res))
278+
})
279+
280+
t.Run("regex values", func(t *testing.T) {
281+
wantErr := errors.New("BSON regex values cannot contain null bytes")
282+
283+
testCases := []struct {
284+
name string
285+
pattern string
286+
options string
287+
}{
288+
{"null bytes in pattern", "a\x00", "i"},
289+
{"null bytes in options", "pattern", "i\x00"},
290+
}
291+
for _, tc := range testCases {
292+
t.Run(tc.name, func(t *testing.T) {
293+
regex := primitive.Regex{
294+
Pattern: tc.pattern,
295+
Options: tc.options,
296+
}
297+
res, err := Marshal(D{{"foo", regex}})
298+
assert.Equal(t, wantErr, err, "expected Marshal error %v, got error %v with result %q", wantErr, err, Raw(res))
299+
})
300+
}
301+
})
302+
}

x/bsonx/bsoncore/bsoncore.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,21 @@ import (
3030
"fmt"
3131
"math"
3232
"strconv"
33+
"strings"
3334
"time"
3435

3536
"go.mongodb.org/mongo-driver/bson/bsontype"
3637
"go.mongodb.org/mongo-driver/bson/primitive"
3738
)
3839

39-
// EmptyDocumentLength is the length of a document that has been started/ended but has no elements.
40-
const EmptyDocumentLength = 5
41-
42-
// nullTerminator is a string version of the 0 byte that is appended at the end of cstrings.
43-
const nullTerminator = string(byte(0))
40+
const (
41+
// EmptyDocumentLength is the length of a document that has been started/ended but has no elements.
42+
EmptyDocumentLength = 5
43+
// nullTerminator is a string version of the 0 byte that is appended at the end of cstrings.
44+
nullTerminator = string(byte(0))
45+
invalidKeyPanicMsg = "BSON element keys cannot contain null bytes"
46+
invalidRegexPanicMsg = "BSON regex values cannot contain null bytes"
47+
)
4448

4549
// AppendType will append t to dst and return the extended buffer.
4650
func AppendType(dst []byte, t bsontype.Type) []byte { return append(dst, byte(t)) }
@@ -51,6 +55,10 @@ func AppendKey(dst []byte, key string) []byte { return append(dst, key+nullTermi
5155
// AppendHeader will append Type t and key to dst and return the extended
5256
// buffer.
5357
func AppendHeader(dst []byte, t bsontype.Type, key string) []byte {
58+
if !isValidCString(key) {
59+
panic(invalidKeyPanicMsg)
60+
}
61+
5462
dst = AppendType(dst, t)
5563
dst = append(dst, key...)
5664
return append(dst, 0x00)
@@ -430,6 +438,10 @@ func AppendNullElement(dst []byte, key string) []byte { return AppendHeader(dst,
430438

431439
// AppendRegex will append pattern and options to dst and return the extended buffer.
432440
func AppendRegex(dst []byte, pattern, options string) []byte {
441+
if !isValidCString(pattern) || !isValidCString(options) {
442+
panic(invalidRegexPanicMsg)
443+
}
444+
433445
return append(dst, pattern+nullTerminator+options+nullTerminator...)
434446
}
435447

@@ -844,3 +856,7 @@ func appendBinarySubtype2(dst []byte, subtype byte, b []byte) []byte {
844856
dst = appendLength(dst, int32(len(b)))
845857
return append(dst, b...)
846858
}
859+
860+
func isValidCString(cs string) bool {
861+
return !strings.ContainsRune(cs, '\x00')
862+
}

x/bsonx/bsoncore/bsoncore_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/google/go-cmp/cmp"
1717
"go.mongodb.org/mongo-driver/bson/bsontype"
1818
"go.mongodb.org/mongo-driver/bson/primitive"
19+
"go.mongodb.org/mongo-driver/internal/testutil/assert"
1920
)
2021

2122
func noerr(t *testing.T, err error) {
@@ -899,6 +900,51 @@ func TestBuild(t *testing.T) {
899900
}
900901
}
901902

903+
func TestNullBytes(t *testing.T) {
904+
// Helper function to execute the provided callback and assert that it panics with the expected message. The
905+
// createBSONFn callback should create a BSON document/array/value and return the stringified version.
906+
assertBSONCreationPanics := func(t *testing.T, createBSONFn func(), expected string) {
907+
t.Helper()
908+
909+
defer func() {
910+
got := recover()
911+
assert.Equal(t, expected, got, "expected panic with error %v, got error %v", expected, got)
912+
}()
913+
createBSONFn()
914+
}
915+
916+
t.Run("element keys", func(t *testing.T) {
917+
createDocFn := func() {
918+
NewDocumentBuilder().AppendString("a\x00", "foo")
919+
}
920+
assertBSONCreationPanics(t, createDocFn, invalidKeyPanicMsg)
921+
})
922+
t.Run("regex values", func(t *testing.T) {
923+
testCases := []struct {
924+
name string
925+
pattern string
926+
options string
927+
}{
928+
{"null bytes in pattern", "a\x00", "i"},
929+
{"null bytes in options", "pattern", "i\x00"},
930+
}
931+
for _, tc := range testCases {
932+
t.Run(tc.name+"-AppendRegexElement", func(t *testing.T) {
933+
createDocFn := func() {
934+
AppendRegexElement(nil, "foo", tc.pattern, tc.options)
935+
}
936+
assertBSONCreationPanics(t, createDocFn, invalidRegexPanicMsg)
937+
})
938+
t.Run(tc.name+"-AppendRegex", func(t *testing.T) {
939+
createValFn := func() {
940+
AppendRegex(nil, tc.pattern, tc.options)
941+
}
942+
assertBSONCreationPanics(t, createValFn, invalidRegexPanicMsg)
943+
})
944+
}
945+
})
946+
}
947+
902948
func compareDecimal128(d1, d2 primitive.Decimal128) bool {
903949
d1H, d1L := d1.GetBytes()
904950
d2H, d2L := d2.GetBytes()

0 commit comments

Comments
 (0)