Commit 34339c45 authored by Alberto Bertogli's avatar Alberto Bertogli

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.
parent 933b9792
Pipeline #99653706 passed with stages
in 8 minutes and 34 seconds
// 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(
"[email protected]", "[email protected]", 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)
}
2EHLO localhost
AUTH SOMETHINGELSE
AUTH PLAIN
dXNlckB0ZXN0c2VydmVyAHlalala==
AUTH PLAIN
dXNlckB0ZXN0c2VydmVyAHVzZXJAdGVzdHNlcnZlcgB3cm9uZ3Bhc3N3b3Jk
AUTH PLAIN
dXNlckB0ZXN0c2VydmVyAHVzZXJAdGVzdHNlcnZlcgBzZWNyZXRwYXNzd29yZA==
AUTH PLAIN
0EHLO localhost
AUTH PLAIN something
AUTH PLAIN something
AUTH PLAIN something
AUTH PLAIN something
AUTH PLAIN something
0DATA
HELO localhost
DATA
MAIL FROM:<[email protected]>
RCPT TO: [email protected]
DATA
From: Mailer daemon <[email protected]>
Subject: I've come to haunt you
Bad header
Muahahahaha
.
QUIT
0HELO localhost
MAIL LALA: <>
MAIL FROM:
MAIL FROM:<pepe>
MAIL FROM:<[email protected]>
MAIL FROM:<aaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aa[email protected]bbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbX>
0HELO localhost
MAIL FROM:<[email protected]>
RCPT LALA: <>
RCPT TO:
RCPT TO:<pepe>
RCPT TO:<[email protected]>
RCPT TO:<henryⅣ@testserver>
RCPT TO:<aaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aaaaXaaaa5aa[email protected]bbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbXbbbb5bbbbX>
0EHLO localhost
MAIL FROM: <>
RCPT TO: [email protected]
DATA
From: Mailer daemon <[email protected]áratro>
Subject: I've come to haunt you
Message-ID: <booooo>
Ñañañañaña!
.
QUIT
0EHLO localhost
MAIL FROM: <>
RCPT TO: [email protected]
DATA
From: Mailer daemon <[email protected]>
Subject: I've come to haunt you
Muahahahaha
.
QUIT
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment