mirror of
https://blitiri.com.ar/repos/chasquid
synced 2026-10-28 15:02:28 +00:00
ce236e91bc
When parsing a header that has a continuation line, today we append that line to it, which copies the whole header each time. On normal messages this is negligible, but a header with a very large number of continuation lines would cause a DoS through work amplification. This patch fixes the problem by making header continuation handling linear: we now accumulate the header lines in a list, and then join them only once. Thanks to John JEAN <john@wargan.com> who reported a similar issue (fixed in a previous patch), which led to finding this bug.
183 lines
4.6 KiB
Go
183 lines
4.6 KiB
Go
package dkim
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"blitiri.com.ar/go/chasquid/internal/normalize"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
)
|
|
|
|
func TestParseMessage(t *testing.T) {
|
|
cases := []struct {
|
|
message string
|
|
headers headers
|
|
body string
|
|
}{
|
|
{
|
|
message: normalize.StringToCRLF(`From: a@b
|
|
To: c@d
|
|
Subject: test
|
|
Continues: This
|
|
continues.
|
|
|
|
body`),
|
|
headers: headers{
|
|
header{Name: "From", Value: " a@b",
|
|
Source: "From: a@b"},
|
|
header{Name: "To", Value: " c@d",
|
|
Source: "To: c@d"},
|
|
header{Name: "Subject", Value: " test",
|
|
Source: "Subject: test"},
|
|
header{Name: "Continues", Value: " This\r\n continues.",
|
|
Source: "Continues: This\r\n continues."},
|
|
},
|
|
body: "body",
|
|
},
|
|
{
|
|
// Multiple continuation lines, with spaces and tabs, in a header
|
|
// that is not the last one. Colons in continuation lines are
|
|
// part of the value.
|
|
message: normalize.StringToCRLF(`From: a@b
|
|
Multi: one
|
|
two:three
|
|
four
|
|
To: c@d
|
|
|
|
body`),
|
|
headers: headers{
|
|
header{Name: "From", Value: " a@b",
|
|
Source: "From: a@b"},
|
|
header{Name: "Multi",
|
|
Value: " one\r\n two:three\r\n\tfour",
|
|
Source: "Multi: one\r\n two:three\r\n\tfour"},
|
|
header{Name: "To", Value: " c@d",
|
|
Source: "To: c@d"},
|
|
},
|
|
body: "body",
|
|
},
|
|
{
|
|
// Empty message.
|
|
message: "",
|
|
headers: headers{},
|
|
body: "",
|
|
},
|
|
}
|
|
|
|
for i, c := range cases {
|
|
headers, body, err := parseMessage(c.message)
|
|
if diff := cmp.Diff(c.headers, headers); diff != "" {
|
|
t.Errorf("parseMessage([%d]) headers mismatch (-want +got):\n%s",
|
|
i, diff)
|
|
}
|
|
if diff := cmp.Diff(c.body, body); diff != "" {
|
|
t.Errorf("parseMessage([%d]) body mismatch (-want +got):\n%s",
|
|
i, diff)
|
|
}
|
|
if err != nil {
|
|
t.Errorf("parseMessage([%d]) error: %v", i, err)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func TestParseMessageWithErrors(t *testing.T) {
|
|
cases := []struct {
|
|
message string
|
|
err error
|
|
}{
|
|
{
|
|
// Continuation without previous header.
|
|
message: " continuation.",
|
|
err: errInvalidHeader,
|
|
},
|
|
{
|
|
// Header without ':'.
|
|
message: "No colon",
|
|
err: errInvalidHeader,
|
|
},
|
|
{
|
|
// Header without ':' in its first line, even if there's one in
|
|
// a continuation line.
|
|
message: "No colon\r\n but: here",
|
|
err: errInvalidHeader,
|
|
},
|
|
}
|
|
|
|
for i, c := range cases {
|
|
_, _, err := parseMessage(c.message)
|
|
if diff := cmp.Diff(c.err, err, cmpopts.EquateErrors()); diff != "" {
|
|
t.Errorf("parseMessage([%d]) err mismatch (-want +got):\n%s",
|
|
i, diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseMessageWithoutBlankLine(t *testing.T) {
|
|
// If there's no blank line, all the lines are headers, including the
|
|
// continuation lines of the last one.
|
|
hs, _, err := parseMessage("From: a@b\r\nTo: c@d\r\n e@f")
|
|
if err != nil {
|
|
t.Fatalf("parseMessage error: %v", err)
|
|
}
|
|
want := headers{
|
|
{Name: "From", Value: " a@b", Source: "From: a@b"},
|
|
{Name: "To", Value: " c@d\r\n e@f", Source: "To: c@d\r\n e@f"},
|
|
}
|
|
if diff := cmp.Diff(want, hs); diff != "" {
|
|
t.Errorf("parseMessage headers mismatch (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func TestParseMessageManyContinuations(t *testing.T) {
|
|
// Regression test for parsing being quadratic on the number of
|
|
// continuation lines of a header. This takes milliseconds, while the
|
|
// quadratic implementation took minutes with this input. We don't check
|
|
// the timing, so a regression will show up as this test being very slow.
|
|
const n = 1_000_000
|
|
value := strings.Repeat("\r\n a", n)
|
|
message := "From: a@b\r\nLong:" + value + "\r\n\r\nbody"
|
|
|
|
headers, body, err := parseMessage(message)
|
|
if err != nil {
|
|
t.Fatalf("parseMessage error: %v", err)
|
|
}
|
|
if len(headers) != 2 {
|
|
t.Fatalf("wanted 2 headers, got %d", len(headers))
|
|
}
|
|
if headers[1].Name != "Long" {
|
|
t.Fatalf("unexpected header name: %q", headers[1].Name)
|
|
}
|
|
if headers[1].Value != value {
|
|
t.Errorf("unexpected value (len %d, want len %d)",
|
|
len(headers[1].Value), len(value))
|
|
}
|
|
if headers[1].Source != "Long:"+value {
|
|
t.Errorf("unexpected source (len %d, want len %d)",
|
|
len(headers[1].Source), len("Long:"+value))
|
|
}
|
|
if body != "body" {
|
|
t.Errorf("unexpected body: %q", body)
|
|
}
|
|
}
|
|
|
|
func TestHeadersFindAll(t *testing.T) {
|
|
hs := headers{
|
|
{Name: "From", Value: "a@b", Source: "From: a@b"},
|
|
{Name: "To", Value: "c@d", Source: "To: c@d"},
|
|
{Name: "Subject", Value: "test", Source: "Subject: test"},
|
|
{Name: "fROm", Value: "z@y", Source: "fROm: z@y"},
|
|
}
|
|
|
|
fromHs := hs.FindAll("froM")
|
|
expected := headers{
|
|
{Name: "From", Value: "a@b", Source: "From: a@b"},
|
|
{Name: "fROm", Value: "z@y", Source: "fROm: z@y"},
|
|
}
|
|
if diff := cmp.Diff(expected, fromHs); diff != "" {
|
|
t.Errorf("headers.Find() mismatch (-want +got):\n%s", diff)
|
|
}
|
|
|
|
}
|