1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2026-10-29 15:02:30 +00:00

dkim: Fix work amplification on h= tag processing

When verifying emails, for each entry in the h= tag, we would scan all
headers, leading to quadratic behaviour.

Similarly, when signing, for each instance of a signed header we would
scan all headers.

This could lead to unauthenticated DoS via work amplification.

This patch fixes the problem by going through the headers only once, so
behaviour is now linear, and also limiting the size of the h= tag for
defense in depth.

The limit is in line with what other implementations use, so it should
not cause any interoperability problems.

Thanks to John JEAN <john@wargan.com> for finding and reporting this
problem. The detailed report can be found in
https://johnjean.io/advisories/chasquid-unauthenticated-remote-dos/.
This commit is contained in:
Alberto Bertogli
2026-10-09 01:05:32 +01:00
parent 5c4193a8b6
commit b153955d5d
6 changed files with 151 additions and 14 deletions
+48
View File
@@ -5,6 +5,7 @@ import (
"crypto"
"encoding/base64"
"errors"
"fmt"
"net"
"strings"
"testing"
@@ -341,6 +342,26 @@ func TestHeadersToInclude(t *testing.T) {
},
},
},
// Check that header names are compared case-insensitively, and that
// headers that are not present are skipped.
{
sigH: header{
Name: "DKIM-Signature",
Value: "v=1; a=rsa-sha256; s=brisbane; d=example.com;",
},
hTag: []string{"from", "X-Missing", "FROM", "subject"},
headers: headers{
{Name: "FROM", Value: "from1"},
{Name: "Subject", Value: "subject1"},
{Name: "From", Value: "from2"},
},
want: []header{
{Name: "From", Value: "from2"},
{Name: "FROM", Value: "from1"},
{Name: "Subject", Value: "subject1"},
},
},
}
for _, c := range cases {
@@ -352,6 +373,33 @@ func TestHeadersToInclude(t *testing.T) {
}
}
func TestHeadersToIncludeLarge(t *testing.T) {
// Regression test for a DoS where headersToInclude was O(len(h=) *
// len(headers)), which with large (attacker-controlled) inputs could take
// hours. 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 = 100_000
sigH := header{Name: "DKIM-Signature", Value: "v=1;"}
hTag := make([]string, n)
hdrs := make(headers, n)
for i := range n {
hTag[i] = "a"
hdrs[i] = header{Name: "a", Value: fmt.Sprint(i)}
}
got := headersToInclude(sigH, hTag, hdrs)
if len(got) != n {
t.Fatalf("wanted %d headers, got %d", n, len(got))
}
// Last instance first.
if got[0].Value != fmt.Sprint(n-1) || got[n-1].Value != "0" {
t.Errorf("unexpected order: first %q, last %q",
got[0].Value, got[n-1].Value)
}
}
func TestAuthenticationResults(t *testing.T) {
resBrisbane := &OneResult{
Domain: "football.example.com",