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
- The operation to perform, a single-key lookup in this case.
- 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.
- 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). ret=full
just means “return the entire value”, in this case, the full path, rather than just the file name.- ABS_DIR is the directory where we will search for the file.
- {$value} is what gets returned if the lookup is successful. In this case we want to return the actual value that was found.
- 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’sfile
. 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 valuefail
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!