Set-uid programs: the good, the bad and the risky
In *nix systems access control is traditionally often done by checking if the user requesting a given operation has read/write/execution privilege with respect to a given file/directory.
In some cases this policy is not fine-grained enough. An example is the common
passwd that allows the user to change its password. Changing the password requires modifying the file
/etc/passwd that stores the passwords (to be honest, this was true on older systems, nowadays things are a bit different, but we stick to this for the sake of the example). This causes a problem with the permission accesses of
/etc/passwd: the file must be clearly not writable by the ordinary user, but... how could the user change its password?
The solution is allow the user to modify
/etc/passwd in a controlled way, via an executable that will change only the user's password. In other words, the user must gain temporally limited root privileges (administrator privileges for you Window people 😄). This is done with the idea of set-user-ID executable. If an executable is marked as setuid, the user running the program gets the privileges of the owner of the executable (usually root). In this way the program can carry out privileged actions on behalf of the user.
The three identities
We need a bit of more detail. In current *nix systems a running executable has 3 identities
- The real user ID. This is the user that is running the code.
- The effective user ID. This is the identity that is used to check the accesses. In a normal program this is equal to the real user ID, but in a setuid program it is initialized to the identity of the owner of the executable.
- The saved user ID. This is initialized with the effective user ID; therefore, in a normal program it is equal to the real UID, while in a setuid program is equal to the ID of the owner.
How do these IDs interact? It is really simple:
The program can change its effective ID to its real or saved ID.
This means that a normal program starts with the effective ID equal to the identity of the user and it cannot be changed; while a setuid program can set it to the real identity (we will say that it drops the privileges) or to the identity of the owner (we will say that it restores the privileges). It is possible for the program to definitively drop the privileges by setting the saved ID to the real ID.
Good practices in
Of course a setuid program is a potential security risk because of this privilege escalation provided by the setuid mechanism. Sure, if the program was correctly written, without weakness nor bugs, it would be safe to have setuid code, but... You know... We are all human beings and a defensive approach to limit the potential damage it is not a bad idea. We will consider two simple good practices
- Drop the privileges as soon as possible, restoring them only when necessary.
- Consider the environment variables as tainted since their value is under the control of the user. For example, the user could set the
PATHvariable to force the code to execute programs under the control of the user. It would be better not using environment variables at all, but if you really need replace the variable values with trusted values (e.g. write in
PATHa list of trusted directory) or, at least, sanitize the value given by the user.
setuid-helper and how can it help me?
setuid-helper is a small Ada library that provides a package
Setuid.Helper designed to help following the above guidelines for setuid programs.
During the package initialization (elaboration in Ada jargon) the package does three things
- It saves the environment variables received from the user
- It deletes every environment variable
- It drops the privileges
This means that when the first instruction of the main is executed, the program runs with the privileges dropped and the environment empty. The only way to restore the privileges is by using the procedure
With_Privileges_Do. This procedure exists in different flavors that differ in how the action to be done is specified.
In the simplest case
procedure With_Privileges_Do (Callback : access procedure; Drop_Permanently : Boolean := True);
With_Privileges_Do expects a
Callback parameter represented by an access (a pointer for you
C people 😄) to a parameter-less procedure. The semantic is really simple: the privileges are restored,
Callback called and the privileged are dropped again. Unless the second parameter is
False, the privileges are permanently dropped and it will not be possible to restore them again. Note that permanent dropping is the default.
This is a typical usage example where the callback is defined locally inside a
declare procedure Unmount_Callback is begin Unmount (Mountpoint_Name); end Unmount_Callback; begin With_Privileges_Do (Callback => Unmount_Callback'Access, Drop_Permanently => True); end;
There are two other versions of
With_Privileges_Do: one expects a handler object (that can store a status), the other expects a function and it can return a value. See the
.adsfile and the README file for more details.
Managing the environment
As said before, it is maybe better not using environment variables at all. This is not always possible and
setuid.helper provides a way to manage it as safely as possible, keeping two different environments: one normal and one privileged.
More precisely, the package mantains three "enviroments"
- The tainted environment. This is a copy of the original environment. It cannot be changed, but it can be read with the functions
Tainted_Variable. This allows to import in a controlled way the values provided by the user in the environment.
- The user environment. This is used when the privileges are dropped. It is initially empty and it can be written with
- The safe environment used when privileges are on. It is initially empty and it can be written with
Set_Safe_Environment expects the value to be of type
String. A string can be converted to a
Safe_Value by calling the function
Just to be clear: it is expected that the value given to
Blessis checked or sanitized, but nothing prevents the programmer to call
Blesswith any unsafe value. The usage of a different type is to avoid involuntary short-circuits where a value of the original environment (to be read with
Tainted_Variable, nomen omen...) is given to
Portability, installation, ...
Just check the beginning of the README file. I work with Linux and in Linux it works; I guess it should work with any modern *nix (if it works for you under another *nix please let me know).
What about Windows? Honestly, I do not know much Windows, but I am afraid that the idea of the setuid bit is very *nix-ish. Is maybe possible to bring the same idea to Windows? Well, let me know, never say never...
setuid-helper is a pretty young library (currently is 0.1.0 since I am not sure about the stability of its API). Any feedback is welcome.