Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

The setuid permission bit tells Linux to run a program with the effective user id of the owner instead of the executor:

> cat setuid-test.c

#include <stdio.h>
#include <unistd.h>

int main(int argc, char** argv) {
    printf("%d", geteuid());
    return 0;
}

> gcc -o setuid-test setuid-test.c
> ./setuid-test

1000

> sudo chown nobody ./setuid-test; sudo chmod +s ./setuid-test
> ./setuid-test

65534

However, this only applies to executables; shell scripts ignore the setuid bit:

> cat setuid-test2

#!/bin/bash
id -u

> ./setuid-test2

1000

> sudo chown nobody ./setuid-test2; sudo chmod +s ./setuid-test2
> ./setuid-test2

1000

Wikipedia says:

Due to the increased likelihood of security flaws, many operating systems ignore the setuid attribute when applied to executable shell scripts.

Assuming I'm willing to accept those risks, is there any way to tell Linux to treat the setuid bit the same on shell scripts as it does on executables?

If not, is there a common workaround for this problem? My current solution is to add a sudoers entry to allow ALL to run a given script as the user I want it run as, with NOPASSWD to avoid the password prompt. The main downsides to that is the need for a sudoers entry every time I want to do this, and the need for the caller to sudo some-script instead of just some-script

share|improve this question

5 Answers 5

Linux ignores the setuid¹ bit on all interpreted executables (i.e. executables starting with a #! line). The comp.unix.questions FAQ explains the security problems with setuid shell scripts. These problems are of two kinds: shebang-related and shell-related.


Setuid shebang

There is a race condition inherent to the way shebang (#!) is typically implemented:

  1. The kernel opens the executable, and finds that it starts with #!.
  2. The kernel closes the executable and opens the interpreter instead.
  3. The kernel inserts the path to the script to the argument list (as argv[1]), and executes the interpreter.

If setuid scripts are allowed with this implementation, an attacker can invoke an arbitrary script by creating a symbolic link to an existing setuid script, executing it, and arranging to change the link after the kernel has performed step 1 and before the interpreter gets around to opening its first argument. For this reason, most unices ignore the setuid bit when they detect a shebang.

One way to secure this implementation would be for the kernel to lock the script file until the interpreter has opened it (note that this must prevent not only unlinking or overwriting the file, but also renaming any directory in the path). But unix systems tend to shy away from mandatory locks, and symbolic links would make a correct lock feature especially difficult and invasive. I don't think anyone does it this way.

A few unix systems (mainly OpenBSD, NetBSD and Mac OS X, all of which require a kernel setting to be enabled) implement secure setuid shebang using an additional feature: the path /dev/fd/N refers to the file already opened on file descriptor N (so opening /dev/fd/N is roughly equivalent to dup(N)). Many unix systems (including Linux) have /dev/fd but not setuid scripts.

  1. The kernel opens the executable, and finds that it starts with #!. Let's say the file descriptor for the executable is 3.
  2. The kernel opens the interpreter.
  3. The kernel inserts /dev/fd/3 the argument list (as argv[1]), and executes the interpreter.

Sven Mascheck's shebang page has a lot of information on shebang across unices, including setuid support.


Setuid interpreters

Let's assume you've managed to make your program run as root, either because your OS supports setuid shebang or because you've used a native binary wrapper (such as sudo). Have you opened a security hole? Maybe. The issue here is not about interpreted vs compiled programs. The issue is whether your runtime system behaves safely if executed with privileges.

  • Any dynamically linked native binary executable is in a way interpreted by the dynamic loader (e.g. /lib/ld.so), which loads the dynamic libraries required by the program. On many unices, you can configure the search path for dynamic libraries through the environment (LD_LIBRARY_PATH is a common name for the environment variable), and even load additional libraries into all executed binaries (LD_PRELOAD). The invoker of the program can execute arbitrary code in that program's context by placing a specially-crafted libc.so in $LD_LIBRARY_PATH (amongst other tactics). All sane systems ignore the LD_* variables in setuid executables.

  • In shells such as sh, csh and derivatives, environment variables automatically become shell parameters. Through parameters such as PATH, IFS, and many more, the invoker of the script has many opportunities to execute arbitrary code in the shell scripts's context. Some shells set these variables to sane defaults if they detect that the script has been invoked with privileges, but I don't know that there is any particular implementation that I would trust.

  • Most runtime environments (whether native, bytecode or interpreted) have similar features. Few take special precautions in setuid executables, though the ones that run native code often don't do anything fancier than dynamic linking (which does take precautions).

  • Perl is a notable exception. It explicitly supports setuid scripts in a secure way. In fact, your script can run setuid even if your OS ignored the setuid bit on scripts. This is because perl ships with a setuid root helper that performs the necessary checks and reinvokes the interpreter on the desired scripts with the desired privileges. This is explained in the perlsec manual. It used to be that setuid perl scripts needed #!/usr/bin/suidperl -wT instead of #!/usr/bin/perl -wT, but on most modern systems, #!/usr/bin/perl -wT is sufficient.

Note that using a native binary wrapper does nothing in itself to prevent these problems. In fact, it can make the situation worse, because it might prevent your runtime environment from detecting that it is invoked with privileges and bypassing its runtime configurability.

A native binary wrapper can make a shell script safe if the wrapper sanitizes the environment. The script must take care not to make too many assumptions (e.g. about the current directory) but this goes. You can use sudo for this provided that it's set up to sanitize the environment. Blacklisting variables is error-prone, so always whitelist. With sudo, make sure that the env_reset option is turned on, that setenv is off, and that env_file and env_keep only contain innocuous variables.


TL,DR:

  • Setuid shebang is insecure but usually ignored.
  • If you run a program with privileges (either through sudo or setuid), write native code or perl, or start the program with a wrapper that sanitizes the environment (such as sudo with the env_reset option).

¹ This discussion applies equally if you substitute “setgid” for “setuid”.

share|improve this answer
1  
@Josh: Secure setuid shell scripts are possible, but only if the both the shell implementer and the script writer are very careful. Rather than native code, I recommend Perl, where the implementers have taken care that setuid scripts should be secure with little effort on the script writer's part. –  Gilles Dec 11 '10 at 13:52
1  
apparently the suidperl stuff has been deprecated and marked for removal for years (but persists non-the-less) –  jmtd May 12 '11 at 14:32
1  
are there any other languages that support setuid safely? –  richard Jun 28 '11 at 9:35
2  
Actually suidperl has been removed as of perl 5.11 (5.12 stable): perl5110delta: > "suidperl" has been removed. It used to provide a mechanism to emulate setuid permission bits on systems that don't support it properly. perl5120delta: > "suidperl" is no longer part of Perl. It used to provide a mechanism to emulate setuid permission bits on systems that don't support it properly. –  Randy Stauner Jul 14 '11 at 17:40
1  
Also note this line from perl 5.6.1 docs (nearly a decade ago)... perl561delta: > Note that suidperl is neither built nor installed by default in any recent version of perl. Use of suidperl is highly discouraged. If you think you need it, try alternatives such as sudo first. See courtesan.com/sudo . –  Randy Stauner Jul 14 '11 at 17:45

If you want to avoid calling sudo some_script you can just do:

  #!/ust/bin/env sh

  sudo /usr/local/scripts/your_script

SETUID programs need to be designed with extreme care as they run with root privileges and users have large control over them. They need to sanity-check everything. You cannot do it with scripts because:

  • Shells are large pieces of software which interact heavily with user. It is nearly impossible to sanity check everything — especially since most of the code is not intended to run in such mode.
  • Scripts are a mostly quick'n'dirty solution and usually are not prepared with such care that they would allow setuid. They have many potentially dangerous features.
  • They depend heavily on other programs. It is not sufficient that the shell was checked. sed, awk, etc. would need to be checked as well

Please note that sudo provides some sanity-checking but it isn't sufficient — check every line in your own code.

As a last note: consider using capabilities. They allow you to give a process running as a user special privileges that would normally require root privileges. However for example, while ping needs to manipulate the network, it does not need to have access to files. I'm not sure however if they are inherited.

share|improve this answer

I prefix a few scripts that are in this boat thus:

#!/bin/sh
[ "root" != "$USER" ] && exec sudo $0 "$@"
share|improve this answer

You can create an alias for sudo + the name of the script. Of course, that is even more work to set up, since you then have to setup an alias, too, but it saves you from having to type sudo.

But if you don't mind horrible security risks, use a setuid shell as the interpreter for the shell script. Don't know whether that'll work for you, but I guess it might.

Let me state that I advise against actually doing this, though. I'm just mentioning it for educational purposes ;-)

share|improve this answer
4  
It will work. Horribly as you stated. SETUID bit allows execution with the owner right. Setuid shell (unless it was designed to work with setuid) will run as root for any user. I.e. anyone can run rm -rf / (and other commands from series DON'T DO IT AT HOME). –  Maciej Piechotka Aug 17 '10 at 10:52
2  
@MaciejPiechotka by DON'T DO IT AT HOME you mean Feel free to do that at work? :) –  peterph Feb 27 at 21:22

One way of solving this problem is to call the shell script from a program that can use the setuid bit.
its something like sudo. For example, here is how you would accomplish this in a C program:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    setuid( 0 );   // you can set it at run time also
    system( "/home/pubuntu/setuid-test2.sh" );
    return 0;
 }

Save it as setuid-test2.c.
compile
Now do the setuid on this program binary:
su - nobody
[enter password]
chown nobody:nobody a.out
chmod 4755 a.out
Now, you should be able to run it, and you'll see your script being executed with nobody permissions.
But here also either you need to hardcode the script path or pass it as command line arg to above exe.
I hope this will help

share|improve this answer
12  
I would advise against the suggestion to allow passing of the script as a command line argument, as that essentially gives anyone who can execute the program the ability to run any script as that defined user. –  dsp Aug 12 '10 at 8:05
    
I would add some sanity checking if you implement that C solution: ensure that the script being run is owned by the right user+group, is not world-writable, and not of the directories in its path are world- or group-writable (essentially the same checks that suPHP performs) - this will reduce the chance of your tool becoming a security problem through other users being able to somehow edit or replace the script. –  David Spillett Aug 12 '10 at 16:14
7  
Note that THIS IS INSECURE even if the full path to the script is hardcoded. The shell will inherit variables from the environment, and many of them allow the invoker to inject arbitrary code. PATH and LD_LIBRARY_PATH are obvious vectors. Some shells execute $ENV or $BASHENV or ~/.zshenv even before they start executing the script proper, so you can't protect from these at all from within the script. The only safe way to invoke a shell script with privileges is to clean up the environment. Sudo knows how to do it safely. So do not write your own wrapper, use sudo. –  Gilles Oct 8 '10 at 20:26
6  
I feel bad that he's suddenly getting downvoted for this -- I did specifically say I wanted to hear insecure versions too, and I was imagining an executable that took a shell script argument when I said it. Obviously it's massively insecure, but I wanted to know what possibilities exist –  Michael Mrozek Oct 8 '10 at 22:58
4  
@Gilles: FYI, Linux unsets LD_LIBRARY_PATH among other things when it encounters the setuid bit. –  grawity Feb 15 '12 at 15:31

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.