[PATCH] Fix unbounded authentication exchanges during PQconnectPoll()
Hello,
This is closely related to the prior conversation at [1]/messages/by-id/a5c5783d-73f3-acbc-997f-1649a7406029@timescale.com. There are a
couple places in CONNECTION_AWAITING_RESPONSE where libpq will read a
huge number of bytes from a server that we really should have hung up on.
The attached patch adds a length check for the v2 error compatibility
case, and updates the v3 error handling to jump to error_return rather
than asking for more data. The existing error_return paths have been
updated for consistency.
Thanks,
--Jacob
[1]: /messages/by-id/a5c5783d-73f3-acbc-997f-1649a7406029@timescale.com
/messages/by-id/a5c5783d-73f3-acbc-997f-1649a7406029@timescale.com
Attachments:
PQconnectPoll-fix-unbounded-authentication-exchanges.patchtext/x-patch; charset=UTF-8; name=PQconnectPoll-fix-unbounded-authentication-exchanges.patchDownload
From 949a5e994235a60731ff827dcfc5157007d937ca Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 18 Nov 2022 13:45:34 -0800
Subject: [PATCH] PQconnectPoll: fix unbounded authentication exchanges
A couple of code paths in CONNECTION_AWAITING_RESPONSE will eagerly read
bytes off a connection that should be closed. Don't let a misbehaving
server chew up client resources here; a v2 error can't be infinitely
long, and a v3 error should be bounded by its original message length.
For the existing error_return cases, I added some additional error
messages for symmetry with the new ones.
---
src/interfaces/libpq/fe-connect.c | 26 +++++++++++++++++++++++---
1 file changed, 23 insertions(+), 3 deletions(-)
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 50b5df3490..5212ae1a52 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -3230,12 +3230,24 @@ keep_going: /* We will come back to here until there is
goto error_return;
}
- if (beresp == 'E' && (msgLength < 8 || msgLength > 30000))
+#define MAX_ERRLEN 30000
+ if (beresp == 'E' && (msgLength < 8 || msgLength > MAX_ERRLEN))
{
/* Handle error from a pre-3.0 server */
conn->inCursor = conn->inStart + 1; /* reread data */
if (pqGets_append(&conn->errorMessage, conn))
{
+ /*
+ * We may not have authenticated the server yet, so
+ * don't let the buffer grow forever.
+ */
+ avail = conn->inEnd - conn->inCursor;
+ if (avail > MAX_ERRLEN)
+ {
+ libpq_append_conn_error(conn, "server sent overlong v2 error message");
+ goto error_return;
+ }
+
/* We'll come back when there is more data */
return PGRES_POLLING_READING;
}
@@ -3255,9 +3267,15 @@ keep_going: /* We will come back to here until there is
goto error_return;
}
+#undef MAX_ERRLEN
/*
* Can't process if message body isn't all here yet.
+ *
+ * After this check passes, any further EOF during parsing
+ * implies that the server sent a bad/truncated message. Reading
+ * more bytes won't help in that case, so don't return
+ * PGRES_POLLING_READING after this point.
*/
msgLength -= 4;
avail = conn->inEnd - conn->inCursor;
@@ -3280,8 +3298,8 @@ keep_going: /* We will come back to here until there is
{
if (pqGetErrorNotice3(conn, true))
{
- /* We'll come back when there is more data */
- return PGRES_POLLING_READING;
+ libpq_append_conn_error(conn, "server sent truncated error message");
+ goto error_return;
}
/* OK, we read the message; mark data consumed */
conn->inStart = conn->inCursor;
@@ -3357,6 +3375,7 @@ keep_going: /* We will come back to here until there is
{
if (pqGetNegotiateProtocolVersion3(conn))
{
+ libpq_append_conn_error(conn, "server sent truncated protocol negotiation message");
goto error_return;
}
/* OK, we read the message; mark data consumed */
@@ -3370,6 +3389,7 @@ keep_going: /* We will come back to here until there is
/* Get the type of request. */
if (pqGetInt((int *) &areq, 4, conn))
{
+ libpq_append_conn_error(conn, "server sent truncated authentication request");
goto error_return;
}
msgLength -= 4;
--
2.25.1
On 14/02/2023 01:22, Jacob Champion wrote:
Hello,
This is closely related to the prior conversation at [1]. There are a
couple places in CONNECTION_AWAITING_RESPONSE where libpq will read a
huge number of bytes from a server that we really should have hung up on.The attached patch adds a length check for the v2 error compatibility
case, and updates the v3 error handling to jump to error_return rather
than asking for more data. The existing error_return paths have been
updated for consistency.
Looks mostly OK to me. Just a few nits on the error message style:
This patch adds the following error messages:
"server sent overlong v2 error message"
"server sent truncated error message"
"server sent truncated protocol negotiation message"
"server sent truncated authentication request"
Existing messages that are most similar to this:
"received invalid response to SSL negotiation: %c"
"received unencrypted data after SSL response"
"received invalid response to GSSAPI negotiation: %c"
"received unencrypted data after GSSAPI encryption response"
"expected authentication request from server, but received %c"
"unexpected message from server during startup"
The existing style emphasizes receiving the message, rather than what
the server sent. In that style, I'd suggest:
"received invalid error message"
"received invalid protocol negotiation message"
"received invalid authentication request"
I don't think the "overlong" or "truncated" bit is helpful. For example,
if the pre-v3.0 error message seems to be "overlong", it's not clear
that's really what happened. More likely, it's just garbage. Similarly,
the "truncated" cases mean that we didn't receive a null-terminator when
we expected one. It might be because the message was truncated, i.e. the
server sent it with a too-short message length. But just as likely, it
forgot to send the null-terminator, or it got confused in some other
way. So I'd go with just "invalid".
For similar reasons, I don't think we need to distinguish between the V3
and pre-V3 errors in the error message. If it's garbage, we probably
didn't guess correctly which one it was.
It's useful to have a unique error message for every different error, so
that if you see that error, you can point to the exact place in the code
where it was generated. If we care about that, we could add some detail
to the messages, like "received invalid error message; null-terminator
not found before end-of-message". I don't think that's necessary,
though, and we've re-used the "expected authentication request from
server, but received %c" for two different checks already.
@@ -3370,6 +3389,7 @@ keep_going: /* We will come back to here until there is /* Get the type of request. */ if (pqGetInt((int *) &areq, 4, conn)) { + libpq_append_conn_error(conn, "server sent truncated authentication request"); goto error_return; } msgLength -= 4;
This is unreachable, because we already checked the length. Better safe
than sorry I guess, but let's avoid the translation overhead of this at
least.
This isn't from your patch, but a pre-existing message in the vicinity
that caught my eye:
if ((beresp == 'R' || beresp == 'v') && (msgLength < 8 || msgLength > 2000))
{
libpq_append_conn_error(conn, "expected authentication request from server, but received %c",
beresp);
goto error_return;
}
If we receive a 'R' or 'v' message that's too long or too short, the
message is confusing because the 'beresp' that it prints is actually
expected, but the length is unexpected.
(Wow, that was a long message for such a simple patch. I may have fallen
into the trap of bikeshedding, sorry :-) )
- Heikki
On Tue, Feb 21, 2023 at 12:35 PM Heikki Linnakangas <hlinnaka@iki.fi> wrote:
I don't think the "overlong" or "truncated" bit is helpful. For example,
if the pre-v3.0 error message seems to be "overlong", it's not clear
that's really what happened. More likely, it's just garbage.
I think this is maybe a distinction without a difference, at least at
the protocol level -- in the event of a missed terminator, any message
could be garbage independently of whether it's too long. But I also
don't mind the "invalid" wording you've proposed, so done that way in
v2. (You're probably going to break out Wireshark for this either
way.)
It's useful to have a unique error message for every different error, so
that if you see that error, you can point to the exact place in the code
where it was generated. If we care about that, we could add some detail
to the messages, like "received invalid error message; null-terminator
not found before end-of-message". I don't think that's necessary,
though, and we've re-used the "expected authentication request from
server, but received %c" for two different checks already.
(Note that I've reworded the duplicate message in patch v2, if that
changes the calculus.)
@@ -3370,6 +3389,7 @@ keep_going: /* We will come back to here until there is /* Get the type of request. */ if (pqGetInt((int *) &areq, 4, conn)) { + libpq_append_conn_error(conn, "server sent truncated authentication request"); goto error_return; } msgLength -= 4;This is unreachable, because we already checked the length. Better safe
than sorry I guess, but let's avoid the translation overhead of this at
least.
Should we just Assert() instead of an error message?
This isn't from your patch, but a pre-existing message in the vicinity
that caught my eye:if ((beresp == 'R' || beresp == 'v') && (msgLength < 8 || msgLength > 2000))
{
libpq_append_conn_error(conn, "expected authentication request from server, but received %c",
beresp);
goto error_return;
}If we receive a 'R' or 'v' message that's too long or too short, the
message is confusing because the 'beresp' that it prints is actually
expected, but the length is unexpected.
Updated. I think there's room for additional improvement here, since
as of the protocol negotiation improvements, we don't just expect an
authentication request anymore.
(Wow, that was a long message for such a simple patch. I may have fallen
into the trap of bikeshedding, sorry :-) )
No worries :D This code is overdue for a tuneup, I think, and message
tweaks are cheap.
Thanks!
--Jacob
Attachments:
since-v1.diff.txttext/plain; charset=US-ASCII; name=since-v1.diff.txtDownload
1: 04942e8e64 ! 1: d7ff5c8f64 PQconnectPoll: fix unbounded authentication exchanges
@@ Commit message
long, and a v3 error should be bounded by its original message length.
For the existing error_return cases, I added some additional error
- messages for symmetry with the new ones.
+ messages for symmetry with the new ones, and cleaned up some message
+ rot.
## src/interfaces/libpq/fe-connect.c ##
@@ src/interfaces/libpq/fe-connect.c: keep_going: /* We will come back to here until there is
+ */
+ if ((beresp == 'R' || beresp == 'v') && (msgLength < 8 || msgLength > 2000))
+ {
+- libpq_append_conn_error(conn, "expected authentication request from server, but received %c",
+- beresp);
++ libpq_append_conn_error(conn, "received invalid authentication request");
goto error_return;
}
@@ src/interfaces/libpq/fe-connect.c: keep_going: /* We will come back to here
+ avail = conn->inEnd - conn->inCursor;
+ if (avail > MAX_ERRLEN)
+ {
-+ libpq_append_conn_error(conn, "server sent overlong v2 error message");
++ libpq_append_conn_error(conn, "received invalid error message");
+ goto error_return;
+ }
+
@@ src/interfaces/libpq/fe-connect.c: keep_going: /* We will come back to here
{
- /* We'll come back when there is more data */
- return PGRES_POLLING_READING;
-+ libpq_append_conn_error(conn, "server sent truncated error message");
++ libpq_append_conn_error(conn, "received invalid error message");
+ goto error_return;
}
/* OK, we read the message; mark data consumed */
@@ src/interfaces/libpq/fe-connect.c: keep_going: /* We will come back to here
{
if (pqGetNegotiateProtocolVersion3(conn))
{
-+ libpq_append_conn_error(conn, "server sent truncated protocol negotiation message");
++ libpq_append_conn_error(conn, "received invalid protocol negotiation message");
goto error_return;
}
/* OK, we read the message; mark data consumed */
@@ src/interfaces/libpq/fe-connect.c: keep_going: /* We will come back to here
/* Get the type of request. */
if (pqGetInt((int *) &areq, 4, conn))
{
-+ libpq_append_conn_error(conn, "server sent truncated authentication request");
++ libpq_append_conn_error(conn, "received invalid authentication request");
goto error_return;
}
msgLength -= 4;
v2-0001-PQconnectPoll-fix-unbounded-authentication-exchan.patchtext/x-patch; charset=US-ASCII; name=v2-0001-PQconnectPoll-fix-unbounded-authentication-exchan.patchDownload
From d7ff5c8f6412345d59d9c5c66bc79cb6dc98802b Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchampion@timescale.com>
Date: Fri, 18 Nov 2022 13:45:34 -0800
Subject: [PATCH v2] PQconnectPoll: fix unbounded authentication exchanges
A couple of code paths in CONNECTION_AWAITING_RESPONSE will eagerly read
bytes off a connection that should be closed. Don't let a misbehaving
server chew up client resources here; a v2 error can't be infinitely
long, and a v3 error should be bounded by its original message length.
For the existing error_return cases, I added some additional error
messages for symmetry with the new ones, and cleaned up some message
rot.
---
src/interfaces/libpq/fe-connect.c | 29 ++++++++++++++++++++++++-----
1 file changed, 24 insertions(+), 5 deletions(-)
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 50b5df3490..762ba51d2e 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -3225,17 +3225,28 @@ keep_going: /* We will come back to here until there is
*/
if ((beresp == 'R' || beresp == 'v') && (msgLength < 8 || msgLength > 2000))
{
- libpq_append_conn_error(conn, "expected authentication request from server, but received %c",
- beresp);
+ libpq_append_conn_error(conn, "received invalid authentication request");
goto error_return;
}
- if (beresp == 'E' && (msgLength < 8 || msgLength > 30000))
+#define MAX_ERRLEN 30000
+ if (beresp == 'E' && (msgLength < 8 || msgLength > MAX_ERRLEN))
{
/* Handle error from a pre-3.0 server */
conn->inCursor = conn->inStart + 1; /* reread data */
if (pqGets_append(&conn->errorMessage, conn))
{
+ /*
+ * We may not have authenticated the server yet, so
+ * don't let the buffer grow forever.
+ */
+ avail = conn->inEnd - conn->inCursor;
+ if (avail > MAX_ERRLEN)
+ {
+ libpq_append_conn_error(conn, "received invalid error message");
+ goto error_return;
+ }
+
/* We'll come back when there is more data */
return PGRES_POLLING_READING;
}
@@ -3255,9 +3266,15 @@ keep_going: /* We will come back to here until there is
goto error_return;
}
+#undef MAX_ERRLEN
/*
* Can't process if message body isn't all here yet.
+ *
+ * After this check passes, any further EOF during parsing
+ * implies that the server sent a bad/truncated message. Reading
+ * more bytes won't help in that case, so don't return
+ * PGRES_POLLING_READING after this point.
*/
msgLength -= 4;
avail = conn->inEnd - conn->inCursor;
@@ -3280,8 +3297,8 @@ keep_going: /* We will come back to here until there is
{
if (pqGetErrorNotice3(conn, true))
{
- /* We'll come back when there is more data */
- return PGRES_POLLING_READING;
+ libpq_append_conn_error(conn, "received invalid error message");
+ goto error_return;
}
/* OK, we read the message; mark data consumed */
conn->inStart = conn->inCursor;
@@ -3357,6 +3374,7 @@ keep_going: /* We will come back to here until there is
{
if (pqGetNegotiateProtocolVersion3(conn))
{
+ libpq_append_conn_error(conn, "received invalid protocol negotiation message");
goto error_return;
}
/* OK, we read the message; mark data consumed */
@@ -3370,6 +3388,7 @@ keep_going: /* We will come back to here until there is
/* Get the type of request. */
if (pqGetInt((int *) &areq, 4, conn))
{
+ libpq_append_conn_error(conn, "received invalid authentication request");
goto error_return;
}
msgLength -= 4;
--
2.25.1
On 22/02/2023 20:49, Jacob Champion wrote:
On Tue, Feb 21, 2023 at 12:35 PM Heikki Linnakangas <hlinnaka@iki.fi> wrote:
@@ -3370,6 +3389,7 @@ keep_going: /* We will come back to here until there is /* Get the type of request. */ if (pqGetInt((int *) &areq, 4, conn)) { + libpq_append_conn_error(conn, "server sent truncated authentication request"); goto error_return; } msgLength -= 4;This is unreachable, because we already checked the length. Better safe
than sorry I guess, but let's avoid the translation overhead of this at
least.Should we just Assert() instead of an error message?
I separated the earlier message-length checks so that you get "invalid
invalid authentication request" or "received invalid protocol
negotiation message", depending on whether it was an 'R' or 'v' message.
With that, "invalid invalid authentication request" becomes translatable
anyway, which makes the point on translation overhead moot. I added a
comment to mention that it's unreachable, though.
I also reformatted the comments a little more.
Pushed with those changes, thanks!
- Heikki
On Wed, Feb 22, 2023 at 11:43 AM Heikki Linnakangas <hlinnaka@iki.fi> wrote:
I separated the earlier message-length checks so that you get "invalid
invalid authentication request" or "received invalid protocol
negotiation message", depending on whether it was an 'R' or 'v' message.
With that, "invalid invalid authentication request" becomes translatable
anyway, which makes the point on translation overhead moot. I added a
comment to mention that it's unreachable, though.
Looks good, thank you!
--Jacob