Skip to content

Commit

Permalink
fix(sec): randomString bias (#2492)
Browse files Browse the repository at this point in the history
* fix(sec): `randomString` bias when using bytes vs int64
* use pooled buffed random reader
  • Loading branch information
trim21 authored Jul 22, 2023
1 parent 626f13e commit b3ec8e0
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 11 deletions.
45 changes: 34 additions & 11 deletions middleware/util.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package middleware

import (
"bufio"
"crypto/rand"
"fmt"
"io"
"strings"
"sync"
)

func matchScheme(domain, pattern string) bool {
Expand Down Expand Up @@ -55,17 +57,38 @@ func matchSubdomain(domain, pattern string) bool {
return false
}

// https://tip.golang.org/doc/go1.19#:~:text=Read%20no%20longer%20buffers%20random%20data%20obtained%20from%20the%20operating%20system%20between%20calls
var randomReaderPool = sync.Pool{New: func() interface{} {
return bufio.NewReader(rand.Reader)
}}

const randomStringCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
const randomStringCharsetLen = 52 // len(randomStringCharset)
const randomStringMaxByte = 255 - (256 % randomStringCharsetLen)

func randomString(length uint8) string {
charset := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
reader := randomReaderPool.Get().(*bufio.Reader)
defer randomReaderPool.Put(reader)

bytes := make([]byte, length)
_, err := rand.Read(bytes)
if err != nil {
// we are out of random. let the request fail
panic(fmt.Errorf("echo randomString failed to read random bytes: %w", err))
}
for i, b := range bytes {
bytes[i] = charset[b%byte(len(charset))]
b := make([]byte, length)
r := make([]byte, length+(length/4)) // perf: avoid read from rand.Reader many times
var i uint8 = 0

for {
_, err := io.ReadFull(reader, r)
if err != nil {
panic("unexpected error happened when reading from bufio.NewReader(crypto/rand.Reader)")
}
for _, rb := range r {
if rb > randomStringMaxByte {
// Skip this number to avoid bias.
continue
}
b[i] = randomStringCharset[rb%randomStringCharsetLen]
i++
if i == length {
return string(b)
}
}
}
return string(bytes)
}
29 changes: 29 additions & 0 deletions middleware/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func Test_matchScheme(t *testing.T) {
Expand Down Expand Up @@ -117,3 +118,31 @@ func TestRandomString(t *testing.T) {
})
}
}

func TestRandomStringBias(t *testing.T) {
t.Parallel()
const slen = 33
const loop = 100000

counts := make(map[rune]int)
var count int64

for i := 0; i < loop; i++ {
s := randomString(slen)
require.Equal(t, slen, len(s))
for _, b := range s {
counts[b]++
count++
}
}

require.Equal(t, randomStringCharsetLen, len(counts))

avg := float64(count) / float64(len(counts))
for k, n := range counts {
diff := float64(n) / avg
if diff < 0.95 || diff > 1.05 {
t.Errorf("Bias on '%c': expected average %f, got %d", k, avg, n)
}
}
}

0 comments on commit b3ec8e0

Please sign in to comment.