diff mbox series

imap-send: increase command size limit

Message ID 7026075c-db4e-4d43-bbd1-d2edb52da9b7@web.de (mailing list archive)
State New, archived
Headers show
Series imap-send: increase command size limit | expand

Commit Message

René Scharfe April 14, 2024, 4:47 p.m. UTC
nfvasprintf() has a 8KB limit, but it's not relevant, as its result is
combined with other strings and added to a 1KB buffer by its caller.
That 1KB limit is not mentioned in RFC 9051, which specifies IMAP.

While 1KB is plenty for user names, passwords and mailbox names,
there's no point in limiting our commands like that.  Call xstrvfmt()
instead of open-coding it and use strbuf to format the command to
send, as we need its length.  Fail hard if it exceeds INT_MAX, because
socket_write() can't take more than that.

Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: René Scharfe <l.s.r@web.de>
---
This time I compiled with NO_CURL=1 and NO_APPLE_COMMON_CRYPTO=1 and
verified with a silly printf that the changed code was actually used
and wrote the present message to an IMAP folder whose name is 1006
characters log, which required a 1026 bytes long APPEND command.  Yay,
freedom!

Ran into a build issue, will send a separate patch for that.

 imap-send.c | 35 ++++++++++++-----------------------
 1 file changed, 12 insertions(+), 23 deletions(-)

--
2.44.0

Comments

brian m. carlson April 14, 2024, 5:44 p.m. UTC | #1
On 2024-04-14 at 16:47:52, René Scharfe wrote:
> nfvasprintf() has a 8KB limit, but it's not relevant, as its result is
> combined with other strings and added to a 1KB buffer by its caller.
> That 1KB limit is not mentioned in RFC 9051, which specifies IMAP.
> 
> While 1KB is plenty for user names, passwords and mailbox names,
> there's no point in limiting our commands like that.  Call xstrvfmt()
> instead of open-coding it and use strbuf to format the command to
> send, as we need its length.  Fail hard if it exceeds INT_MAX, because
> socket_write() can't take more than that.
> 
> Suggested-by: Jeff King <peff@peff.net>
> Signed-off-by: René Scharfe <l.s.r@web.de>
> ---
> This time I compiled with NO_CURL=1 and NO_APPLE_COMMON_CRYPTO=1 and
> verified with a silly printf that the changed code was actually used
> and wrote the present message to an IMAP folder whose name is 1006
> characters log, which required a 1026 bytes long APPEND command.  Yay,
> freedom!

I'm curious, is there a particular problem that you (or someone else)
ran into that caused you to make this change?  I agree it seems prudent
in general, but if there's a particular real-world broken case that this
hits (e.g., mailbox names in a given language), I think the commit
message would be a great place to mention this real-world impact, which
would lend support to your argument that this is a valuable change to
make.
Junio C Hamano April 15, 2024, 6:38 p.m. UTC | #2
"brian m. carlson" <sandals@crustytoothpaste.net> writes:

>>  ...
>> Suggested-by: Jeff King <peff@peff.net>
>> Signed-off-by: René Scharfe <l.s.r@web.de>
>> ---
>> This time I compiled with NO_CURL=1 and NO_APPLE_COMMON_CRYPTO=1 and
>> verified with a silly printf that the changed code was actually used
>> and wrote the present message to an IMAP folder whose name is 1006
>> characters log, which required a 1026 bytes long APPEND command.  Yay,
>> freedom!
>
> I'm curious, is there a particular problem that you (or someone else)
> ran into that caused you to make this change?  I agree it seems prudent
> in general, but if there's a particular real-world broken case that this
> hits (e.g., mailbox names in a given language), I think the commit
> message would be a great place to mention this real-world impact, which
> would lend support to your argument that this is a valuable change to
> make.

I personally am not curious about real-world problem in this case,
but I won't stop you asking ;-)

I view this more about code simplification.  We no longer need a
custom nfvasprintf() helper nobody else cares about, leaving the
resulting code easier to read.

Will queue but will wait for a day or two to see if René wants to
add clarification to the proposed commit log message before merging
it to 'next'.

Thanks, both.
Jeff King April 15, 2024, 6:45 p.m. UTC | #3
On Mon, Apr 15, 2024 at 11:38:28AM -0700, Junio C Hamano wrote:

> > I'm curious, is there a particular problem that you (or someone else)
> > ran into that caused you to make this change?  I agree it seems prudent
> > in general, but if there's a particular real-world broken case that this
> > hits (e.g., mailbox names in a given language), I think the commit
> > message would be a great place to mention this real-world impact, which
> > would lend support to your argument that this is a valuable change to
> > make.
> 
> I personally am not curious about real-world problem in this case,
> but I won't stop you asking ;-)
> 
> I view this more about code simplification.  We no longer need a
> custom nfvasprintf() helper nobody else cares about, leaving the
> resulting code easier to read.
> 
> Will queue but will wait for a day or two to see if René wants to
> add clarification to the proposed commit log message before merging
> it to 'next'.

Yeah, as the suggested-by person, I can affirm that this is not
something I saw in real life. I agree the primary motivation is the code
simplification, and dropping a pointless limit is the bonus. I'd
probably have written it in that style, but I'm OK either way.

-Peff
Jeff King April 15, 2024, 6:55 p.m. UTC | #4
On Sun, Apr 14, 2024 at 06:47:52PM +0200, René Scharfe wrote:

> While 1KB is plenty for user names, passwords and mailbox names,
> there's no point in limiting our commands like that.  Call xstrvfmt()
> instead of open-coding it and use strbuf to format the command to
> send, as we need its length.  Fail hard if it exceeds INT_MAX, because
> socket_write() can't take more than that.

Hmm. I applaud your attention to detail, but this INT_MAX thing is ugly. ;)
Shouldn't socket_write() just use size_t / ssize_t?

In particular, this made me wonder what we would do for larger items.
Like, say, the actual message to be uploaded. And indeed, we use a
strbuf to read in the messages and pass the whole buffer for each to
socket_write(). So we'd possibly quietly truncate such a message.

Fixing it is a little more complicated than switching to size_t, because
the underlying SSL_write() uses an int. So we'd probably need some
looping, similar to xwrite().

In practice I doubt this is ever an issue. 2GB emails are not likely to
be usable in general. And I kind of doubt that this is a reasonable
vector for attacks, since the inputs to imap-send would generally come
from the user themselves (and certainly truncating the attack message is
probably not that interesting, though I imagine one could convince
write_in_full() to do an out-of-bounds read as a size_t becomes a
negative int which becomes a large size_t again).

So I am happy enough with this (especially given my general opinions of
imap-send in the first place).

-Peff
René Scharfe April 16, 2024, 3:31 p.m. UTC | #5
Am 15.04.24 um 20:45 schrieb Jeff King:
> On Mon, Apr 15, 2024 at 11:38:28AM -0700, Junio C Hamano wrote:
>
>>> I'm curious, is there a particular problem that you (or someone else)
>>> ran into that caused you to make this change?  I agree it seems prudent
>>> in general, but if there's a particular real-world broken case that this
>>> hits (e.g., mailbox names in a given language), I think the commit
>>> message would be a great place to mention this real-world impact, which
>>> would lend support to your argument that this is a valuable change to
>>> make.
>>
>> I personally am not curious about real-world problem in this case,
>> but I won't stop you asking ;-)
>>
>> I view this more about code simplification.  We no longer need a
>> custom nfvasprintf() helper nobody else cares about, leaving the
>> resulting code easier to read.
>>
>> Will queue but will wait for a day or two to see if René wants to
>> add clarification to the proposed commit log message before merging
>> it to 'next'.
>
> Yeah, as the suggested-by person, I can affirm that this is not
> something I saw in real life. I agree the primary motivation is the code
> simplification, and dropping a pointless limit is the bonus. I'd
> probably have written it in that style, but I'm OK either way.

Right, it's just intended as a code cleanup.  I mention the limits only
because lifting them is a change of behavior.  Though you can now use
passwords and folder names longer than 1000 characters, I don't expect
that to catch on.

René
René Scharfe April 16, 2024, 4:08 p.m. UTC | #6
Am 15.04.24 um 20:55 schrieb Jeff King:
> On Sun, Apr 14, 2024 at 06:47:52PM +0200, René Scharfe wrote:
>
>> While 1KB is plenty for user names, passwords and mailbox names,
>> there's no point in limiting our commands like that.  Call xstrvfmt()
>> instead of open-coding it and use strbuf to format the command to
>> send, as we need its length.  Fail hard if it exceeds INT_MAX, because
>> socket_write() can't take more than that.
>
> Hmm. I applaud your attention to detail, but this INT_MAX thing is ugly. ;)

It is.  I thought about using cast_size_t_to_int() instead, but decided
to preserve the original error message.

> Shouldn't socket_write() just use size_t / ssize_t?

Probably size_t.

> In particular, this made me wonder what we would do for larger items.
> Like, say, the actual message to be uploaded. And indeed, we use a
> strbuf to read in the messages and pass the whole buffer for each to
> socket_write(). So we'd possibly quietly truncate such a message.

Hmm, perhaps we should at least sprinkle in some more overflow checks?

> Fixing it is a little more complicated than switching to size_t, because
> the underlying SSL_write() uses an int. So we'd probably need some
> looping, similar to xwrite().

Or SSL_write_ex(), which takes and returns size_t.  It was added in
OpenSSL 1.1.1, which reached its end of life half a year ago.

https://www.openssl.org/docs/man1.1.1/man3/SSL_write.html
https://www.openssl.org/blog/blog/2023/03/28/1.1.1-EOL/

> In practice I doubt this is ever an issue. 2GB emails are not likely to
> be usable in general.

Tough.  Who likes to get multi-GB patches in their inbox?  Heard of
people exchanging CD images by email decades ago, though, so I
wouldn't rule this out totally.  Perhaps that's the last puzzle piece
to convert game studios to perform email reviews of asset-heavy
binary diffs? ;-)

> And I kind of doubt that this is a reasonable
> vector for attacks, since the inputs to imap-send would generally come
> from the user themselves (and certainly truncating the attack message is
> probably not that interesting, though I imagine one could convince
> write_in_full() to do an out-of-bounds read as a size_t becomes a
> negative int which becomes a large size_t again).

Right.  If you can get a user to upload more than 2GB of hostile data to
their mail server then you don't need to bother crafting an integer
overflow exploit.

Still, checking for overflow instead of truncating silently seems like a
good idea even for half-dead low-impact code.  #leftoverbits

René
Jeff King April 17, 2024, 12:24 a.m. UTC | #7
On Tue, Apr 16, 2024 at 06:08:05PM +0200, René Scharfe wrote:

> > Shouldn't socket_write() just use size_t / ssize_t?
> 
> Probably size_t.

Yes, but you need ssize_t to handle the negative return values.

> > In particular, this made me wonder what we would do for larger items.
> > Like, say, the actual message to be uploaded. And indeed, we use a
> > strbuf to read in the messages and pass the whole buffer for each to
> > socket_write(). So we'd possibly quietly truncate such a message.
> 
> Hmm, perhaps we should at least sprinkle in some more overflow checks?

Perhaps, but...

> > Fixing it is a little more complicated than switching to size_t, because
> > the underlying SSL_write() uses an int. So we'd probably need some
> > looping, similar to xwrite().
> 
> Or SSL_write_ex(), which takes and returns size_t.  It was added in
> OpenSSL 1.1.1, which reached its end of life half a year ago.
> 
> https://www.openssl.org/docs/man1.1.1/man3/SSL_write.html
> https://www.openssl.org/blog/blog/2023/03/28/1.1.1-EOL/

You'd think that when I ran "man SSL_write" while writing the other
email I would have noticed that SSL_write_ex() is mentioned right there
in the synopsis. But somehow I didn't.

I don't think we document a required version for ssl. That version is
"only" 5.5 years old, but I think it would be OK here (especially given
that imap-send is an optional component with a build-time knob).

In which case I think fixing socket_write() would fix this problem, and
then building your other patch on top, it doesn't need to worry about
INT_MAX at all.

Looking at the conversion, I think there's a slight gotcha with
retaining the "0" return value from SSL_write_ex() to pass to
socket_perror() in the error path. Which makes me wonder about that
error path at all; it closes descriptors but doesn't handle SSL at all.
Should it be using socket_shutdown()? And should that function set
sock->ssl to NULL and the descriptors to -1? The rabbit hole of
imap-send is infinite.

> > In practice I doubt this is ever an issue. 2GB emails are not likely to
> > be usable in general.
> 
> Tough.  Who likes to get multi-GB patches in their inbox?  Heard of
> people exchanging CD images by email decades ago, though, so I
> wouldn't rule this out totally.  Perhaps that's the last puzzle piece
> to convert game studios to perform email reviews of asset-heavy
> binary diffs? ;-)

OK, I laughed out loud at this. Perhaps a sign of too much Git.

-Peff
diff mbox series

Patch

diff --git a/imap-send.c b/imap-send.c
index 4caa8668e6..0afd088d8a 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -68,20 +68,6 @@  static void imap_warn(const char *, ...);

 static char *next_arg(char **);

-static int nfvasprintf(char **strp, const char *fmt, va_list ap)
-{
-	int len;
-	char tmp[8192];
-
-	len = vsnprintf(tmp, sizeof(tmp), fmt, ap);
-	if (len < 0)
-		die("Fatal: Out of memory");
-	if (len >= sizeof(tmp))
-		die("imap command overflow!");
-	*strp = xmemdupz(tmp, len);
-	return len;
-}
-
 struct imap_server_conf {
 	const char *name;
 	const char *tunnel;
@@ -503,11 +489,11 @@  static struct imap_cmd *issue_imap_cmd(struct imap_store *ctx,
 {
 	struct imap *imap = ctx->imap;
 	struct imap_cmd *cmd;
-	int n, bufl;
-	char buf[1024];
+	int n;
+	struct strbuf buf = STRBUF_INIT;

 	cmd = xmalloc(sizeof(struct imap_cmd));
-	nfvasprintf(&cmd->cmd, fmt, ap);
+	cmd->cmd = xstrvfmt(fmt, ap);
 	cmd->tag = ++imap->nexttag;

 	if (cb)
@@ -519,27 +505,30 @@  static struct imap_cmd *issue_imap_cmd(struct imap_store *ctx,
 		get_cmd_result(ctx, NULL);

 	if (!cmd->cb.data)
-		bufl = xsnprintf(buf, sizeof(buf), "%d %s\r\n", cmd->tag, cmd->cmd);
+		strbuf_addf(&buf, "%d %s\r\n", cmd->tag, cmd->cmd);
 	else
-		bufl = xsnprintf(buf, sizeof(buf), "%d %s{%d%s}\r\n",
-				 cmd->tag, cmd->cmd, cmd->cb.dlen,
-				 CAP(LITERALPLUS) ? "+" : "");
+		strbuf_addf(&buf, "%d %s{%d%s}\r\n", cmd->tag, cmd->cmd,
+			    cmd->cb.dlen, CAP(LITERALPLUS) ? "+" : "");
+	if (buf.len > INT_MAX)
+		die("imap command overflow!");

 	if (0 < verbosity) {
 		if (imap->num_in_progress)
 			printf("(%d in progress) ", imap->num_in_progress);
 		if (!starts_with(cmd->cmd, "LOGIN"))
-			printf(">>> %s", buf);
+			printf(">>> %s", buf.buf);
 		else
 			printf(">>> %d LOGIN <user> <pass>\n", cmd->tag);
 	}
-	if (socket_write(&imap->buf.sock, buf, bufl) != bufl) {
+	if (socket_write(&imap->buf.sock, buf.buf, buf.len) != buf.len) {
 		free(cmd->cmd);
 		free(cmd);
 		if (cb)
 			free(cb->data);
+		strbuf_release(&buf);
 		return NULL;
 	}
+	strbuf_release(&buf);
 	if (cmd->cb.data) {
 		if (CAP(LITERALPLUS)) {
 			n = socket_write(&imap->buf.sock, cmd->cb.data, cmd->cb.dlen);