Lessons learned from Apache mod_chroot vs. PHP’s mail()

Some lessons learned from setting up Apache with mod_chroot and then trying to get PHP’s mail() function to work. Sure, there are several mod_chroot caveats listed, but that wasn’t quite enough.

Short story:

Problems:

  • mail() doesn’t work since it won’t have access to sendmail or whatever MTA binaries that are most likely located outside your chroot jail
  • You’ll either need to use something else than mail() – e.g. a replacement library/class – or make some workaround to get mail() working
  • Most likely a workaround is not what you want, since it means having to patch e.g. WordPress, Mediawiki and all sorts of existing software that already uses mail() (WordPress has a plugin to replace the call to mail() but I couldn’t get it working either)
  • Even after trying a sendmail binary replacement (I went with mini_sendmail), something was just not working (I tried ssmtp and nbsmtp also, to no luck. In hindsight: Probably related to problem #2 listed below)
  • Yes, that included adding various libraries and stuff to the chrooted area. (hint: ldd. l2chroot is also nice)

Problem 1: mini_sendmail failing to detect the current username

Tested using chroot from the shell first:

# chroot /var/www /bin/mini_sendmail
/bin/mini_sendmail: can't determine username

So what’s that about?

Well, there’s a segment of code that tries to figure out the login/username using getlogin() and it even has some #ifdef’s to try getpwuid(getuid()) instead, but for some reason both of them fail.

Manually setting it to some dummy value seemed to be the solution:

username = "blah"; // doesn't work:  getlogin();

Problem 2: replacement binaries were simply just not working at all from Apache/PHP

The symptom was …. PHP’s mail() was just returning “false”. Nothing in the Apache error logs or anywhere else.

I even tried creating a C program that just logged the arguments and any STDIN input to a file, pointed the php.ini file to it, but nothing appeared in the output file either.

I tried calling the binaries using system() and exec() from PHP, but they returned “” (blank string) and nothing was logged in the Apache logs. WTF?

Turns out you need to have /bin/sh in your chroot jail, or else PHP refuses to run. I guess this is because it uses system(3) which forks a shell, instead of using exec(3) or execve(2).

I don’t know. Adding /bin/sh worked. End of story.

Problem 3: mini_sendmail screwing up while parsing email addresses from mail headers

When I tried to get this working with Mediawiki, I ran into another problem: the activation mails weren’t being sent out. A little bit tcpdump was all it took to notice what was going on there: the following command was being rejected by the MTA:

RCPT TO:<Real Name <foo@example.org>

Here the arg_dumper C binary I wrote came handy. This was what it showed being passed to mail() from Mediawiki:

To: Real Name <foo@example.org>

Two problems here:

  1. The ‘RCPT TO’ command should not include the descriptive name of the recipient; only the address should be included
  2. Where did the last > go in the SMTP ‘RCPT TO:’ command, anyhow?

Turns out it was the add_recipient() method in mini_sendmail that doesn’t handle this very well:

    /* Skip leading whitespace. */
    while ( len > 0 && ( *recipient == ' ' || *recipient == '\t' ) )
        {
        ++recipient;
        --len;
        }

    /* Strip off any angle brackets. */
    while ( len > 0 && *recipient == '<' )
        {
        ++recipient;
        --len;
        }
    while ( len > 0 && recipient[len-1] == '>' )
        --len;

So, what does it do? First it chops off any whitespace and leading opening angle brackets (‘<’). Then it chops off any trailing closing angle brackets. (‘>’).

This effectively chops off the last character in the Mediawiki template. So, I modified that to something like:


    /* Skip leading whitespace. */
    while ( len > 0 && ( *recipient == ' ' || *recipient == '\t' ) )
        {
        ++recipient;
        --len;
        }

    /* Strip off any angle brackets.  (fixed?) */

  if ( (len > 0) && (recipient[len-1] == '>') )
    {
    while ( len > 0 && *recipient != '<' )
        {
        ++recipient;
        --len;
        }

    /* skip leading < we stopped at */
    --len;
    ++recipient;

    while ( len > 0 && recipient[len-1] == '>' )
        --len;
    }
}

I.e.: only do this bracket-stripping if there is a trailing bracket, and chop off anything before the first bracket.

..which works with all of these:

To: Real Name <foo@example.org>
To: <foo@example.org>
To: foo@example.org

Produces:

RCPT TO:<foo@example.org>

Yay.

I can haz mod_chroot’ed mail().