git » chasquid » commit 34339c4

smtpsrv: Add fuzz testing

author Alberto Bertogli
2019-11-06 00:39:18 UTC
committer Alberto Bertogli
2019-11-30 11:38:46 UTC
parent 933b9792205eb1c01634fea77a650187e076c02e

smtpsrv: Add fuzz testing

This patch adds a fuzz test for the smtpsrv package.

It brings up a server for test, and then fuzz the data sent over the
network.

internal/smtpsrv/fuzz.go +265 -0
internal/smtpsrv/testdata/fuzz/corpus/t-auth_multi_dialog +9 -0
internal/smtpsrv/testdata/fuzz/corpus/t-auth_not_tls +2 -0
internal/smtpsrv/testdata/fuzz/corpus/t-auth_too_many_failures +6 -0
internal/smtpsrv/testdata/fuzz/corpus/t-bad_data +15 -0
internal/smtpsrv/testdata/fuzz/corpus/t-bad_mail_from +6 -0
internal/smtpsrv/testdata/fuzz/corpus/t-bad_rcpt_to +8 -0
internal/smtpsrv/testdata/fuzz/corpus/t-empty_helo +3 -0
internal/smtpsrv/testdata/fuzz/corpus/t-helo +2 -0
internal/smtpsrv/testdata/fuzz/corpus/t-null_address +13 -0
internal/smtpsrv/testdata/fuzz/corpus/t-sendmail +12 -0
internal/smtpsrv/testdata/fuzz/corpus/t-unknown_command +2 -0

diff --git a/internal/smtpsrv/fuzz.go b/internal/smtpsrv/fuzz.go
new file mode 100644
index 0000000..2412456
--- /dev/null
+++ b/internal/smtpsrv/fuzz.go
@@ -0,0 +1,265 @@
+// Fuzz testing for package smtpsrv.  Based on server_test.
+
+// +build gofuzz
+
+package smtpsrv
+
+import (
+	"bufio"
+	"bytes"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"flag"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"math/big"
+	"net"
+	"net/textproto"
+	"os"
+	"strings"
+	"time"
+
+	"blitiri.com.ar/go/chasquid/internal/aliases"
+	"blitiri.com.ar/go/chasquid/internal/courier"
+	"blitiri.com.ar/go/chasquid/internal/testlib"
+	"blitiri.com.ar/go/chasquid/internal/userdb"
+	"blitiri.com.ar/go/log"
+)
+
+var (
+	// Server addresses. Will be filled in at init time.
+	smtpAddr          = ""
+	submissionAddr    = ""
+	submissionTLSAddr = ""
+
+	// TLS configuration to use in the clients.
+	// Will contain the generated server certificate as root CA.
+	tlsConfig *tls.Config
+)
+
+//
+// === Fuzz test ===
+//
+
+func Fuzz(data []byte) int {
+	// Byte 0: mode
+	// The rest is what we will send the server, one line per command.
+	if len(data) < 1 {
+		return 0
+	}
+
+	var mode SocketMode
+	addr := ""
+	switch data[0] {
+	case '0':
+		mode = ModeSMTP
+		addr = smtpAddr
+	case '1':
+		mode = ModeSubmission
+		addr = submissionAddr
+	case '2':
+		mode = ModeSubmissionTLS
+		addr = submissionTLSAddr
+	default:
+		return 0
+	}
+	data = data[1:]
+
+	var err error
+	var conn net.Conn
+	if mode.TLS {
+		conn, err = tls.Dial("tcp", addr, tlsConfig)
+	} else {
+		conn, err = net.Dial("tcp", addr)
+	}
+	if err != nil {
+		panic(fmt.Errorf("failed to dial: %v", err))
+	}
+	defer conn.Close()
+
+	tconn := textproto.NewConn(conn)
+	defer tconn.Close()
+
+	in_data := false
+	scanner := bufio.NewScanner(bytes.NewBuffer(data))
+	for scanner.Scan() {
+		line := scanner.Text()
+
+		// Skip STARTTLS if it happens on a non-TLS connection - the jump is
+		// not going to happen via fuzzer, it will just cause a timeout (which
+		// is considered a crash).
+		if strings.TrimSpace(strings.ToUpper(line)) == "STARTTLS" && !mode.TLS {
+			continue
+		}
+
+		if err = tconn.PrintfLine(line); err != nil {
+			break
+		}
+		if in_data {
+			if line == "." {
+				in_data = false
+			} else {
+				continue
+			}
+		}
+
+		if _, _, err = tconn.ReadResponse(-1); err != nil {
+			break
+		}
+
+		in_data = strings.HasPrefix(strings.ToUpper(line), "DATA")
+	}
+	if (err != nil && err != io.EOF) || scanner.Err() != nil {
+		return 1
+	}
+
+	return 0
+}
+
+//
+// === Test environment ===
+//
+
+// generateCert generates a new, INSECURE self-signed certificate and writes
+// it to a pair of (cert.pem, key.pem) files to the given path.
+// Note the certificate is only useful for testing purposes.
+func generateCert(path string) error {
+	tmpl := x509.Certificate{
+		SerialNumber: big.NewInt(1234),
+		Subject: pkix.Name{
+			Organization: []string{"chasquid_test.go"},
+		},
+
+		DNSNames:    []string{"localhost"},
+		IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
+
+		NotBefore: time.Now(),
+		NotAfter:  time.Now().Add(24 * time.Hour),
+
+		KeyUsage: x509.KeyUsageKeyEncipherment |
+			x509.KeyUsageDigitalSignature |
+			x509.KeyUsageCertSign,
+
+		BasicConstraintsValid: true,
+		IsCA:                  true,
+	}
+
+	priv, err := rsa.GenerateKey(rand.Reader, 1024)
+	if err != nil {
+		return err
+	}
+
+	derBytes, err := x509.CreateCertificate(
+		rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv)
+	if err != nil {
+		return err
+	}
+
+	// Create a global config for convenience.
+	srvCert, err := x509.ParseCertificate(derBytes)
+	if err != nil {
+		return err
+	}
+	rootCAs := x509.NewCertPool()
+	rootCAs.AddCert(srvCert)
+	tlsConfig = &tls.Config{
+		ServerName: "localhost",
+		RootCAs:    rootCAs,
+	}
+
+	certOut, err := os.Create(path + "/cert.pem")
+	if err != nil {
+		return err
+	}
+	defer certOut.Close()
+	pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
+
+	keyOut, err := os.OpenFile(
+		path+"/key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+	if err != nil {
+		return err
+	}
+	defer keyOut.Close()
+
+	block := &pem.Block{
+		Type:  "RSA PRIVATE KEY",
+		Bytes: x509.MarshalPKCS1PrivateKey(priv),
+	}
+	pem.Encode(keyOut, block)
+	return nil
+}
+
+// waitForServer waits 10 seconds for the server to start, and returns an error
+// if it fails to do so.
+// It does this by repeatedly connecting to the address until it either
+// replies or times out. Note we do not do any validation of the reply.
+func waitForServer(addr string) {
+	start := time.Now()
+	for time.Since(start) < 10*time.Second {
+		conn, err := net.Dial("tcp", addr)
+		if err == nil {
+			conn.Close()
+			return
+		}
+
+		time.Sleep(100 * time.Millisecond)
+	}
+
+	panic(fmt.Errorf("%v not reachable", addr))
+}
+
+func init() {
+	flag.Parse()
+
+	log.Default.Level = log.Debug
+
+	// Generate certificates in a temporary directory.
+	tmpDir, err := ioutil.TempDir("", "chasquid_smtpsrv_fuzz:")
+	if err != nil {
+		panic(fmt.Errorf("Failed to create temp dir: %v\n", tmpDir))
+	}
+	defer os.RemoveAll(tmpDir)
+
+	err = generateCert(tmpDir)
+	if err != nil {
+		panic(fmt.Errorf("Failed to generate cert for testing: %v\n", err))
+	}
+
+	smtpAddr = testlib.GetFreePort()
+	submissionAddr = testlib.GetFreePort()
+	submissionTLSAddr = testlib.GetFreePort()
+
+	s := NewServer()
+	s.Hostname = "localhost"
+	s.MaxDataSize = 50 * 1024 * 1025
+	s.AddCerts(tmpDir+"/cert.pem", tmpDir+"/key.pem")
+	s.AddAddr(smtpAddr, ModeSMTP)
+	s.AddAddr(submissionAddr, ModeSubmission)
+	s.AddAddr(submissionTLSAddr, ModeSubmissionTLS)
+
+	localC := &courier.Procmail{}
+	remoteC := &courier.SMTP{}
+	s.InitQueue(tmpDir+"/queue", localC, remoteC)
+	s.InitDomainInfo(tmpDir + "/domaininfo")
+
+	udb := userdb.New("/dev/null")
+	udb.AddUser("testuser", "testpasswd")
+	s.aliasesR.AddAliasForTesting(
+		"to@localhost", "testuser@localhost", aliases.EMAIL)
+	s.AddDomain("localhost")
+	s.AddUserDB("localhost", udb)
+
+	// Disable SPF lookups, to avoid leaking DNS queries.
+	disableSPFForTesting = true
+
+	go s.ListenAndServe()
+
+	waitForServer(smtpAddr)
+	waitForServer(submissionAddr)
+	waitForServer(submissionTLSAddr)
+}
diff --git a/internal/smtpsrv/testdata/fuzz/corpus/t-auth_multi_dialog b/internal/smtpsrv/testdata/fuzz/corpus/t-auth_multi_dialog
new file mode 100644
index 0000000..bcacbc1
--- /dev/null
+++ b/internal/smtpsrv/testdata/fuzz/corpus/t-auth_multi_dialog
@@ -0,0 +1,9 @@
+2EHLO localhost
+AUTH SOMETHINGELSE
+AUTH PLAIN
+dXNlckB0ZXN0c2VydmVyAHlalala==
+AUTH PLAIN
+dXNlckB0ZXN0c2VydmVyAHVzZXJAdGVzdHNlcnZlcgB3cm9uZ3Bhc3N3b3Jk
+AUTH PLAIN
+dXNlckB0ZXN0c2VydmVyAHVzZXJAdGVzdHNlcnZlcgBzZWNyZXRwYXNzd29yZA==
+AUTH PLAIN
diff --git a/internal/smtpsrv/testdata/fuzz/corpus/t-auth_not_tls b/internal/smtpsrv/testdata/fuzz/corpus/t-auth_not_tls
new file mode 100644
index 0000000..6d74550
--- /dev/null
+++ b/internal/smtpsrv/testdata/fuzz/corpus/t-auth_not_tls
@@ -0,0 +1,2 @@
+0EHLO localhost
+AUTH PLAIN
diff --git a/internal/smtpsrv/testdata/fuzz/corpus/t-auth_too_many_failures b/internal/smtpsrv/testdata/fuzz/corpus/t-auth_too_many_failures
new file mode 100644
index 0000000..8dc038c
--- /dev/null
+++ b/internal/smtpsrv/testdata/fuzz/corpus/t-auth_too_many_failures
@@ -0,0 +1,6 @@
+0EHLO localhost
+AUTH PLAIN something
+AUTH PLAIN something
+AUTH PLAIN something
+AUTH PLAIN something
+AUTH PLAIN something
diff --git a/internal/smtpsrv/testdata/fuzz/corpus/t-bad_data b/internal/smtpsrv/testdata/fuzz/corpus/t-bad_data
new file mode 100644
index 0000000..b8c6fcf
--- /dev/null
+++ b/internal/smtpsrv/testdata/fuzz/corpus/t-bad_data
@@ -0,0 +1,15 @@
+0DATA
+HELO localhost
+DATA
+MAIL FROM:<a@b>
+RCPT TO: user@testserver
+DATA
+From: Mailer daemon <somewhere@horns.com>
+Subject: I've come to haunt you
+Bad header
+
+Muahahahaha
+
+
+.
+QUIT
diff --git a/internal/smtpsrv/testdata/fuzz/corpus/t-bad_mail_from b/internal/smtpsrv/testdata/fuzz/corpus/t-bad_mail_from
new file mode 100644
index 0000000..15898ac
--- /dev/null
+++ b/internal/smtpsrv/testdata/fuzz/corpus/t-bad_mail_from
@@ -0,0 +1,6 @@
+0HELO localhost
+MAIL LALA: <>
+MAIL FROM:
+MAIL FROM:<pepe>
+MAIL FROM:<a@xn--->
+MAIL FROM:<aaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaX@bbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbX>
diff --git a/internal/smtpsrv/testdata/fuzz/corpus/t-bad_rcpt_to b/internal/smtpsrv/testdata/fuzz/corpus/t-bad_rcpt_to
new file mode 100644
index 0000000..4072bbc
--- /dev/null
+++ b/internal/smtpsrv/testdata/fuzz/corpus/t-bad_rcpt_to
@@ -0,0 +1,8 @@
+0HELO localhost
+MAIL FROM:<test@testy.com>
+RCPT LALA: <>
+RCPT TO:
+RCPT TO:<pepe>
+RCPT TO:<a@xn--->
+RCPT TO:<henryⅣ@testserver>
+RCPT TO:<aaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaX@bbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbX>
diff --git a/internal/smtpsrv/testdata/fuzz/corpus/t-empty_helo b/internal/smtpsrv/testdata/fuzz/corpus/t-empty_helo
new file mode 100644
index 0000000..236adea
--- /dev/null
+++ b/internal/smtpsrv/testdata/fuzz/corpus/t-empty_helo
@@ -0,0 +1,3 @@
+0HELO
+EHLO
+HELO localhost
diff --git a/internal/smtpsrv/testdata/fuzz/corpus/t-helo b/internal/smtpsrv/testdata/fuzz/corpus/t-helo
new file mode 100644
index 0000000..7a5ba57
--- /dev/null
+++ b/internal/smtpsrv/testdata/fuzz/corpus/t-helo
@@ -0,0 +1,2 @@
+0HELO localhost
+QUIT
diff --git a/internal/smtpsrv/testdata/fuzz/corpus/t-null_address b/internal/smtpsrv/testdata/fuzz/corpus/t-null_address
new file mode 100644
index 0000000..1f28266
--- /dev/null
+++ b/internal/smtpsrv/testdata/fuzz/corpus/t-null_address
@@ -0,0 +1,13 @@
+0EHLO localhost
+MAIL FROM: <>
+RCPT TO: user@testserver
+DATA
+From: Mailer daemon <somewhere@báratro>
+Subject: I've come to haunt you
+Message-ID: <booooo>
+
+Ñañañañaña!
+
+
+.
+QUIT
diff --git a/internal/smtpsrv/testdata/fuzz/corpus/t-sendmail b/internal/smtpsrv/testdata/fuzz/corpus/t-sendmail
new file mode 100644
index 0000000..3553cf9
--- /dev/null
+++ b/internal/smtpsrv/testdata/fuzz/corpus/t-sendmail
@@ -0,0 +1,12 @@
+0EHLO localhost
+MAIL FROM: <>
+RCPT TO: user@testserver
+DATA
+From: Mailer daemon <somewhere@horns.com>
+Subject: I've come to haunt you
+
+Muahahahaha
+
+
+.
+QUIT
diff --git a/internal/smtpsrv/testdata/fuzz/corpus/t-unknown_command b/internal/smtpsrv/testdata/fuzz/corpus/t-unknown_command
new file mode 100644
index 0000000..a87adce
--- /dev/null
+++ b/internal/smtpsrv/testdata/fuzz/corpus/t-unknown_command
@@ -0,0 +1,2 @@
+0EHLO localhost
+WHATISTHIS