Exim: Frequently Raised Security Concerns
-----------------------------------------
On a couple of occasions, statements have been made about purported security
problems in Exim, causing some alarm among users. Some valid concerns were
raised, but several of the points were unfounded and based on a lack of
understanding of how Exim works.
This document is a stock reply for use in similar situations in future. It
contains material that should also be covered in the Exim manual in the chapter
entitled "Security considerations", but not everybody reads that, it seems (and
at the present time, the manual lags behind this file).
The answers below describe what is supposed to be the case in Exim's code. I
make as many mistakes as anybody, so of course there may be bugs which mean
that things aren't quite as they are supposed to be. I would hope to fix any
specific security exposures that are reported to me as soon as I can.
Work was done on Exim version 1.63 to improve its handling of string buffers.
The comments below about string handling apply to this and later versions.
[Exim 1.63 is not released yet. It is the version I am currently developing.]
Philip Hazel
April 1997
1. Doesn't Exim's use of seteuid() make it insecure?
----------------------------------------------------
. Exim always uses setuid() (sic) when performing a local delivery. There are
no exceptions.
. If the administrator has defined a specific uid and gid for Exim's use, which
is the recommended way to run it, then, with the default security setting:
(a) The daemon uses setuid() to become exim after setting up the listening
socket on port 25. Thus all incoming messages are read under Exim's uid.
Once a message has been received, Exim re-execs itself to regain the root
privilege in order to do the delivery.
(b) Likewise, when receiving a message locally, setuid() is used to run as
exim rather than root.
(c) When delivering messages to remote hosts, Exim uses setuid() to run as
exim.
(d) The only use made of seteuid() is while Exim is directing and routing a
message, i.e. deciding where to deliver it. It runs seteuid() to exim on
the grounds that that does give some small security improvement over
running as root. Also, when reading a user's .forward file, it uses
seteuid() to become that user. This is necessary when the file is not
publicly readable and is on a remote NFS file system that is mounted
without root privilege. When Exim actually gets round to doing the
message delivery, it changes state using setuid() as described above.
It is true that the administrator can change the behaviour of (a) and (b) by
setting a security option which causes seteuid() to be used instead of
setuid(). Also, if the administrator has not provided a uid for Exim's use,
then it runs as root except when doing local deliveries. Either of these
approaches save some resources because some re-execs are not needed. However,
they are not recommended for normal use.
There is no way to cause Exim to use seteuid() for local deliveries, or (when
an Exim user is defined) for remote deliveries.
2. You shouldn't rely on argv[0] containing the program's path name
-------------------------------------------------------------------
Exim doesn't. It examines the last component of argv[0], and if it matches
one of a set of specific strings, Exim assumes certain options. For example,
calling Exim with the last component of argv[0] set to "rsmtp" is exactly
equivalent to calling it with the option "-bS". That is all.
3. The use of the %f formatting conversion can lead to buffer overflow
----------------------------------------------------------------------
The only use made of %f by Exim is in formatting load average values. These are
actually stored in integer variables as 1000 times the load average.
Consequently, their range is limited and so is the length of the converted
output.
4. The value of exim's path in global workspace could be overwritten
--------------------------------------------------------------------
Exim uses its own path name only when it needs to re-exec in order to regain
root privilege. Therefore, by definition, it is not root when it does so. If
some bug allowed the path to get overwritten, it would lead to an arbitrary
program's being run as exim, not as root. If there's still paranoia about this,
I could keep two separate copies, or do some checksumming of global data.
5. The use of sprintf() is highly dangerous
-------------------------------------------
[This paragraph applies to the forthcoming 1.63 release.]
A large number of occurrences of "sprintf" in the code are actually calls to
string_sprintf(), a function which returns the result in malloc'd store. The
intermediate formatting is done into a large fixed buffer by a function that
runs through the format string itself, and checks the length of each conversion
before performing it, thus preventing buffer overruns.
The remaining uses of sprintf() happen in controlled circumstances, where the
output buffer is known to be sufficiently long to contain the converted string.
6. Arbitrary strings are passed to debug_printf() and log_write()
-----------------------------------------------------------------
[This paragraph applies to the forthcoming 1.63 release.]
These functions do their formatting by calling the function string_vformat(),
which runs through the format string itself, and checks the length of each
conversion.
7. How about strcat() and strcpy()?
-----------------------------------
These should be used only in cases where the output buffer is known to be large
enough to hold the result.
--
Philip Hazel University Computing Service,
ph10@??? New Museums Site, Cambridge CB2 3QG,
P.Hazel@??? England. Phone: +44 1223 334714