mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-17 14:37:02 +00:00
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.
This commit is contained in:
265
internal/smtpsrv/fuzz.go
Normal file
265
internal/smtpsrv/fuzz.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
9
internal/smtpsrv/testdata/fuzz/corpus/t-auth_multi_dialog
vendored
Normal file
9
internal/smtpsrv/testdata/fuzz/corpus/t-auth_multi_dialog
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
2EHLO localhost
|
||||||
|
AUTH SOMETHINGELSE
|
||||||
|
AUTH PLAIN
|
||||||
|
dXNlckB0ZXN0c2VydmVyAHlalala==
|
||||||
|
AUTH PLAIN
|
||||||
|
dXNlckB0ZXN0c2VydmVyAHVzZXJAdGVzdHNlcnZlcgB3cm9uZ3Bhc3N3b3Jk
|
||||||
|
AUTH PLAIN
|
||||||
|
dXNlckB0ZXN0c2VydmVyAHVzZXJAdGVzdHNlcnZlcgBzZWNyZXRwYXNzd29yZA==
|
||||||
|
AUTH PLAIN
|
||||||
2
internal/smtpsrv/testdata/fuzz/corpus/t-auth_not_tls
vendored
Normal file
2
internal/smtpsrv/testdata/fuzz/corpus/t-auth_not_tls
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
0EHLO localhost
|
||||||
|
AUTH PLAIN
|
||||||
6
internal/smtpsrv/testdata/fuzz/corpus/t-auth_too_many_failures
vendored
Normal file
6
internal/smtpsrv/testdata/fuzz/corpus/t-auth_too_many_failures
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
0EHLO localhost
|
||||||
|
AUTH PLAIN something
|
||||||
|
AUTH PLAIN something
|
||||||
|
AUTH PLAIN something
|
||||||
|
AUTH PLAIN something
|
||||||
|
AUTH PLAIN something
|
||||||
15
internal/smtpsrv/testdata/fuzz/corpus/t-bad_data
vendored
Normal file
15
internal/smtpsrv/testdata/fuzz/corpus/t-bad_data
vendored
Normal file
@@ -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
|
||||||
6
internal/smtpsrv/testdata/fuzz/corpus/t-bad_mail_from
vendored
Normal file
6
internal/smtpsrv/testdata/fuzz/corpus/t-bad_mail_from
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
0HELO localhost
|
||||||
|
MAIL LALA: <>
|
||||||
|
MAIL FROM:
|
||||||
|
MAIL FROM:<pepe>
|
||||||
|
MAIL FROM:<a@xn--->
|
||||||
|
MAIL FROM:<aaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaX@bbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbX>
|
||||||
8
internal/smtpsrv/testdata/fuzz/corpus/t-bad_rcpt_to
vendored
Normal file
8
internal/smtpsrv/testdata/fuzz/corpus/t-bad_rcpt_to
vendored
Normal file
@@ -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>
|
||||||
3
internal/smtpsrv/testdata/fuzz/corpus/t-empty_helo
vendored
Normal file
3
internal/smtpsrv/testdata/fuzz/corpus/t-empty_helo
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
0HELO
|
||||||
|
EHLO
|
||||||
|
HELO localhost
|
||||||
2
internal/smtpsrv/testdata/fuzz/corpus/t-helo
vendored
Normal file
2
internal/smtpsrv/testdata/fuzz/corpus/t-helo
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
0HELO localhost
|
||||||
|
QUIT
|
||||||
13
internal/smtpsrv/testdata/fuzz/corpus/t-null_address
vendored
Normal file
13
internal/smtpsrv/testdata/fuzz/corpus/t-null_address
vendored
Normal file
@@ -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
|
||||||
12
internal/smtpsrv/testdata/fuzz/corpus/t-sendmail
vendored
Normal file
12
internal/smtpsrv/testdata/fuzz/corpus/t-sendmail
vendored
Normal file
@@ -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
|
||||||
2
internal/smtpsrv/testdata/fuzz/corpus/t-unknown_command
vendored
Normal file
2
internal/smtpsrv/testdata/fuzz/corpus/t-unknown_command
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
0EHLO localhost
|
||||||
|
WHATISTHIS
|
||||||
Reference in New Issue
Block a user