Things that can affect the setuid program
Let's consider some ways the calling user could affect the behavior of the setuid process. I'll divide the things to consider in three groups: 1) the program itself, 2) the input to the program, and 3) the environment it runs in.
If the unprivileged user can modify the binary that will be run, that would be a simple way to change the privileged process. This includes cases where the path to the program file goes through user-writable directories and symlinks. Making sure the program file and the path to it is not writable by unauthorized users is mandatory. (And
sudo doesn't seem to check.)
Setuid binaries are actually more immune to modification in this way, since the setuid bit is a property of the inode, not the path, so symlink tricking will not work. (Also, it seems that some Linuxes drop the setuid bit if another user writes to the file, but I wouldn't count on that.)
Input issues include all the input the program needs and uses to function, be it data from standard input, command line arguments or something else.
If the program needs any input from the user (instead of doing exactly one thing, always), we can't avoid this. Network services need to be careful with their input in the same way. (Witness SQL injections etc. for an example.) I think input handling is especially difficult for things like shell scripts where all data is text and all text is one quote away from turning into a command, so to say.
If the input is sensitive, we might consider issues like command line arguments being visible to other users on the system.
The environment is a wider issue. Obviously it includes environment variables, with stuff like
PATH (which can change which subprocess runs),
POSIXLY_CORRECT (which might change the output format of shell commands),
HOME (which might be used to access files), and
LD_* (which modify the behaviour of the dynamic loader) among others. Those are the easy things.
But consider something like resource limits, Linux cgroups, chroots, SELinux contexts, namespaces and who knows what else. Setting, say a tight limit for stack use or for the number of processes might make the privileged process crash at an unexpected point or fail to launch subprocesses. The unexpected crash might happen after the process has locked some resource but before it has a chance to commit and unlock it...
(Though, many of the latter cannot be changed by non-privileged users, so they might not be a significant problem. A setuid executable that is accessible from a limited environment might still need to properly deal with the limited environment.)
What to do about it
At the very least you should
- Make sure the executable is writable only by authorized users, including both the file itself and the path leading to it. Making it non-readable is not really necessary, unless the program contains embedded secrets.
- Reset the environment when starting, including setting
PATH to known value. Using absolute paths feels safe, but I can't tell what effect it would have if the search path is known.
sudo should do it.
LD_* variables are processed by the dynamic linker before you get a chance to unset them, but the linker should ignore them for setuid processes.)
- Validate its input. Of course. Though that may also be easier said than done, especially if you are considering giving user input to a shell script.
- Take sensitive input (passwords) through a pipe or stdin.
- Reset resource limits or take the risk of failing at an unexpected point.
In theory, it should be possible to make a "safe" setuid or sudo-ran program in a traditional environment, but the less-common system specific features might be harder to validate.
(And incidentally, I might say that a setuid program will not always easily open a security breach, since many systems have stuff like
sudo itself that are setuid, yet are not considered relatively safe.)
Alternative to setuid
The other possibility besides sudo or setuid, is to talk to the privileged process through a pipe (or socket). This has the advantage that the running binary is easily secured, and its execution environment is known and the unprivileged user has no way of affecting it.
However, with pipes input from all writers is stuck together without separation. You could define requests to be separated by newlines, but someone could still write a partial line to the pipe, prepending data to the next request from another process. Also, pipes don't easily allow bidirectional communication, so getting a status reply from the privileged process would not be easy.
Sockets don't have this problem, but they can't be accessed by the usual file system functions either. Instead, they need to be accessed via socket functions. (i.e.
echo foo > /my/socket will not work.
socat might be useful in scripts).