Updating to Exim 4.94 and Taintedness

I finally got around to updating my server to Debian Bullseye from Buster. The thing that had been holding me was this notice in the upgrade notes:

Please consider the version of Exim in bullseye a major Exim upgrade. It introduces the concept of tainted data read from untrusted sources. The basic strategy for dealing with this change is to use the result of a lookup in further processing instead of the original (remote provided) value.

To ease upgrading there is a new main configuration option to temporarily downgrade taint errors to warnings, letting the old configuration work with the newer Exim. To make use of this feature add

.ifdef _OPT_MAIN_ALLOW_INSECURE_TAINTED_DATA
 allow_insecure_tainted_data = yes
.endif

to the Exim configuration (e.g. to /etc/exim4/exim4.conf.localmacros) before upgrading and check the logfile for taint warnings. This is a temporary workaround which is already marked for removal on introduction.

This sounded scary, so I put it off for years, but then I decided to just do it since the docs above said one could set the allow_insecure option and then check the logs for specific problems. Alas, after the upgrade my exim started bouncing mails due to a lookup error:

temporarily rejected RCPT <someonetomechangosubanana.com>: Tainted name '/etc/exim4/WHATEVER' for file read not permitted

Long story short, instead of injecting a tainted variable when expanding a string; in this case, for a router’s file option like so:

store_and_forward_1:
        driver = redirect
        file=/etc/exim4/lists/${lc:$local_part}@${lc:$domain}.remote
        forbid_pipe
        forbid_file
        unseen

lists:
    driver = redirect
    file=/etc/exim4/lists/${lc:$local_part}@${lc:$domain}
    forbid_pipe
    forbid_file

domain_catchall:
        driver = redirect
        file=/etc/exim4/lists/${lc:$domain}
        forbid_pipe
        forbid_file

One has to do a lookup instead:

store_and_forward_1:
        driver = redirect
        file=${lookup {${local_part}@${domain}.remote} dsearch,ret=full {/etc/exim4/lists} {$value} fail}
        forbid_pipe
        forbid_file
        unseen
lists:
    driver = redirect
    file=${lookup {${local_part}@${domain}} dsearch,ret=full {/etc/exim4/lists} {$value} fail}
    forbid_pipe
    forbid_file

domain_catchall:
        driver = redirect
        file=${lookup {${lc:domain}} dsearch,ret=full {/etc/exim4/lists} {$value} fail}
        forbid_pipe
        forbid_file

The lookup syntax is devilish.

${lookup {KEY} dsearch,ret=full {ABS_DIR} {$value} fail}

  1       2    3       4         5         6       7
  1. The operation to perform, a single-key lookup in this case.
  2. The key to search for. In the examples above, we build the key from user-input data, which is fine by the tainting rules, as long as that’s used just to look up data in a database, table or file listing. The point of taintedness is to NOT use the tainted value itself to build values that will go, for example, in filenames.
  3. This is the type of lookup, dsearch is “directory search” - this will look for a file named KEY in the ABS_DIR directory, and return (this is the important part) the NAME OF THE FILE IT FOUND, not the value of the key (which might be evil).
  4. ret=full just means “return the entire value”, in this case, the full path, rather than just the file name.
  5. ABS_DIR is the directory where we will search for the file.
  6. {$value} is what gets returned if the lookup is successful. In this case we want to return the actual value that was found.
  7. If the lookup fails, then this value gets returned. If not specified, it always returns the empty string, which resulted in another error:"" is not an absolute path because then it thinks we’re assigning "" to the router’s file. Instead, what we want is for the thing to fail so the router gets marked as unprocessed and the processing continues in the normal order. Specifying the special value fail gives that behavior.

With these changes, the routers work as they did before, while following the rules about when and how to use a tainted user-input value.

Luckily for the upgrade to Bookworm and Exim 4.96, there is no such breaking change in exim configuration!