1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
|
From: =?utf-8?q?Timo_R=C3=B6hling?= <timo@gaussglocke.de>
Date: Sun, 21 Mar 2021 15:27:55 +0100
Subject: SECURITY: Fix DoS on overly long input from Postfix
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit
Thanks to Mateusz Jończyk who reported this issue and gave valuable
feedback for its resolution.
PostSRSd would hang on an overly long GET request, because the
fread()/fwrite() logic in the subprocess would get confused by the
remaining input line in its buffer.
Theoretically, this error should never occur, as Postfix is supposed to
send valid email addresses only, which are shorter than the buffer, even
assuming every single character is percent-encoded. However, Postfix
sometimes does seem to send malformed request with multiple concatenated
email addresses. I'm not sure if there's a reliable way to trigger this
condition by an external attacker, but it is a security bug in PostSRSd
nevertheless.
Fixes CVE-2021-35525.
Origin: https://github.com/roehling/postsrsd/commit/077be98d8c8a9847e4ae0c7dc09e7474cbe27db2
Forwarded: not-needed
Last-Update: 2021-07-14
---
postsrsd.c | 52 ++++++++++++++++++++++++++++++-------------------
run_postsrsd_tests.bats | 40 +++++++++++++++++++++++++++++++++----
2 files changed, 68 insertions(+), 24 deletions(-)
diff --git a/postsrsd.c b/postsrsd.c
index c009d8f..5ebf7f6 100644
--- a/postsrsd.c
+++ b/postsrsd.c
@@ -518,9 +518,9 @@ int main (int argc, char **argv)
fds[sc].events = POLLIN;
}
while(TRUE) {
int conn;
- FILE *fp;
+ FILE *fp_read, *fp_write;
char linebuf[1024], *line;
char keybuf[1024], *key;
if (poll(fds, socket_count, 1000) < 0) {
@@ -540,41 +540,53 @@ int main (int argc, char **argv)
int i;
// close listen sockets so that we don't stop the main daemon process from restarting
for (i = 0; i < socket_count; ++i) close (sockets[i]);
- fp = fdopen(conn, "r+");
- if (fp == NULL) exit(EXIT_FAILURE);
- fds[0].fd = conn;
- fds[0].events = POLLIN;
- if (poll(fds, 1, timeout * 1000) <= 0) return EXIT_FAILURE;
- line = fgets(linebuf, sizeof(linebuf), fp);
- while (line) {
- fseek (fp, 0, SEEK_CUR); /* Workaround for Solaris */
+ /* create separate input/output streams */
+ fp_read = fdopen(conn, "r");
+ if (fp_read == NULL)
+ return EXIT_FAILURE;
+ fp_write = fdopen(dup(conn), "w");
+ if (fp_write == NULL) return EXIT_FAILURE;
+ errno = 0;
+ alarm(timeout);
+ if (errno != 0)
+ return EXIT_FAILURE;
+ while ((line = fgets(linebuf, sizeof(linebuf), fp_read))) {
char* token;
+ alarm(0);
+ if (strlen(line) >= sizeof(linebuf) - 1) {
+ fprintf(fp_write, "500 Invalid request\n");
+ fflush(fp_write);
+ return EXIT_FAILURE;
+ }
token = strtok(line, " \r\n");
if (token == NULL || strcmp(token, "get") != 0) {
- fprintf (fp, "500 Invalid request\n");
- fflush (fp);
+ fprintf (fp_write, "500 Invalid request\n");
+ fflush (fp_write);
return EXIT_FAILURE;
}
token = strtok(NULL, "\r\n");
if (!token) {
- fprintf (fp, "500 Invalid request\n");
- fflush (fp);
+ fprintf (fp_write, "500 Invalid request\n");
+ fflush (fp_write);
return EXIT_FAILURE;
}
key = url_decode(keybuf, sizeof(keybuf), token);
if (!key) {
- fprintf (fp, "500 Invalid request\n");
- fflush(fp);
+ fprintf (fp_write, "500 Invalid request\n");
+ fflush(fp_write);
return EXIT_FAILURE;
}
- handler[sc](srs, fp, key, domain, excludes);
- fflush (fp);
- if (poll(fds, 1, timeout * 1000) <= 0) break;
- line = fgets(linebuf, sizeof(linebuf), fp);
+ handler[sc](srs, fp_write, key, domain, excludes);
+ fflush (fp_write);
+ errno = 0;
+ alarm(timeout);
+ if (errno != 0)
+ return EXIT_FAILURE;
}
- fclose (fp);
+ fclose (fp_write);
+ fclose (fp_read);
return EXIT_SUCCESS;
}
close (conn);
}
diff --git a/run_postsrsd_tests.bats b/run_postsrsd_tests.bats
index f4b04bb..3d52a50 100755
--- a/run_postsrsd_tests.bats
+++ b/run_postsrsd_tests.bats
@@ -2,9 +2,9 @@
# vim: filetype=bash:
if [ ! -x "$POSTSRSD" ]
then
- for builddir in . build* obj*
+ for builddir in . build* obj* _build*
do
if [ -x "${builddir}/postsrsd" ]
then
POSTSRSD="${builddir}/postsrsd"
@@ -14,9 +14,9 @@ then
fi
if [ ! -x "$POSTSRSD" ]
then
cat>&2 <<- EOF
- cannot find postsrsd executable (looked in ., build*, obj*)
+ cannot find postsrsd executable (looked in ., build*, obj*, _build*)
please build the executable first, or set the POSTSRSD
environment variable if it is in a different location.
EOF
@@ -25,14 +25,21 @@ fi
LANG=C.UTF-8
+fillchar()
+{
+ local count="$1"
+ local char="$2"
+ eval 'printf "'"$char"'%.0s" {1..'"$count"'}'
+}
+
start_postsrsd_at()
{
echo 'tops3cr3t' > "$BATS_TMPDIR/postsrsd.secret"
local faketime="$1"
shift
- faketime "${faketime}" ${POSTSRSD} -D -f 10001 -r 10002 -p "$BATS_TMPDIR/postsrsd.pid" -s "$BATS_TMPDIR/postsrsd.secret" -d example.com "$@"
+ faketime "${faketime}" ${POSTSRSD} -D -t1 -f 10001 -r 10002 -p "$BATS_TMPDIR/postsrsd.pid" -s "$BATS_TMPDIR/postsrsd.secret" -d example.com "$@"
}
stop_postsrsd()
{
@@ -158,9 +165,9 @@ teardown()
read<&9 line
[[ "$line" =~ ^"500 Domain excluded" ]]
}
-@test "SRS invalid requests" {
+@test "Malformed or invalid requests" {
start_postsrsd_at "2020-01-01 00:01:00 UTC"
exec 9<>/dev/tcp/127.0.0.1/10001
echo>&9 "get"
read<&9 line
@@ -172,5 +179,30 @@ teardown()
exec 9<>/dev/tcp/127.0.0.1/10001
echo>&9 "get encoding%error@otherdomain.com"
read<&9 line
[[ "$line" =~ ^500 ]]
+ exec 9<>/dev/tcp/127.0.0.1/10001
+ # Try to overflow the input buffer
+ echo>&9 "get too_long@`fillchar 1024 a`.com"
+ read<&9 line
+ [[ "$line" =~ ^500 ]]
+}
+
+@test "Pipelining multiple requests" {
+ start_postsrsd_at "2020-01-01 00:01:00 UTC"
+ exec 9<>/dev/tcp/127.0.0.1/10001
+ # Send two requests at once and see if PostSRSd answers both
+ echo>&9 -e "get test@domain1.com\nget test@domain2.com"
+ read<&9 line
+ [[ "$line" =~ ^200 ]]
+ read<&9 line
+ [[ "$line" =~ ^200 ]]
+}
+
+@test "Session timeout" {
+ start_postsrsd_at "2020-01-01 00:01:00 UTC"
+ exec 9<>/dev/tcp/127.0.0.1/10001
+ # Wait until PostSRSd disconnects due to inactivity
+ sleep 2
+ echo >&9 "get test@example.com"
+ ! read <&9 line
}
|