Solution for executing root commands as unprivileged user - sudo 'ing script - easily opens possibility of security breach and unexpected behavior and results (this is also true for any other solution such as binary wrapper with setuid);

It's clear that scripts added using visudo, like this

www-data ALL=(ALL) NOPASSWD: /usr/local/sbin/mycommand
  • should be readable, writable and executable only by root - which is both owner and group (chown root:root mycommand; chmod 700 mycommand)
  • should have its parent directory ownership root:root with 755 mode
  • should validate input - arguments and stdin - and reject and abort execution upon any invalid/unexpected data provided
  • should use absolute instead of relative paths / aliases (?)
  • Defaults env_reset in /etc/sudoers should be set (?help needed here)

What else can be done to secure a sudo-powered script?

  • 6
    Why does your webserver need to run things as root in the first place? That's a very scary thing to be doing, and there might well be a better solution. – marinus Jul 12 '16 at 15:47
  • 1
    @steve well, because if I can do it (whitelisting) in real time, without telling the users to hold for 2, 5 , 15... minutes, I should do it. – Miloš Đakonović Jul 12 '16 at 19:58
  • 1
    @steve if you could provide me with info? – Miloš Đakonović Jul 12 '16 at 20:05
  • 4
    Please never allow www-data to sudo to root, and even more without password. In a case of any flaw in the site, an SQL injection, a PHP vulnerability, or some script exploration, it becomes trivial to escalate to root. – Rui F Ribeiro Jul 12 '16 at 20:18
  • 2
    Here's a decent essay on the subject: vidarholen.net/contents/blog/?p=30 – Edward Falk Jul 13 '16 at 15:56

I'd amend your list of criteria for protecting a script a little. Given this - or a similar - entry in /etc/sudoers:

www-data ALL=(ALL) NOPASSWD: /usr/local/sbin/mycommand

we can state that the script:

  • must be writeable only by the root user
  • must be readable and executable by the root user
  • must be in a hierarchy of directories that can only be written by root
  • must validate its input "sufficiently" for the use, and reject anything else
  • should have the smallest set of privileges necessary to carry out its task (not necessarily setuid root)
  • should define its PATH before using any external commands
  • should set all variables to a known value before using them
  • should generate an audit trail to show not only when and how it was called, but also the resulting action (think logger)

Additionally, in many cases there is no real need for a script to run as root - it can run setgid, or even setuid to some other account. In the general case consider these options to avoid granting full root access to the script.

For SELinux environments it may be possible to create a policy that prevents the script from doing anything unexpected. Capabilities such as CAP_NET_ADMIN are more finely grained than blanket root privileges and might also be worth considering.

In the specific case you've outlined, where you want to validate a single IPv4 address and pass it to iptables, you might be able to get away with validating the IP address as a series of non-specific octets. In this case 444.555.666.999 might be accepted as plausible, knowing that iptables itself will reject anything that isn't a real IP address. At one extreme you might decide that matching the RE /^[0-9.]+$/ is enough to be happy passing the value to iptables. At the other, well there are plenty of answers on StackExchange and in other places that address the issue of validating an IP address. Some better than others.

Special cases to consider are RFC1918 addresses, multicast addresses, and your own external IP address range. Oh, and the reserved block formerly known as Class E. Do you need IPv6 support?

What will happen if your script is called hundreds of times a minute? Do you need to prepare for this eventuality? Will your iptables chain overflow? If you think you're going to need hundreds of rules in your chain it will be [more efficient to use the ipset extension to iptables rather than a linear list. Here's a good tutorial. In terms of protection, it allows you to build sets of thousands (if not tens of thousands) of similar rules that can run without significantly slowing the traffic flowing through your rulesets.

Suddenly your apparently straightforward requirement is quite complex.

| improve this answer | |
  • most probably the bounty answer, thank you. I have some additional questions regarding your answer, I'll ask them them later here in comments. Regarding iptables "request overflow" I've prepared things for this scenario in my first thoughts about this script - max 50 whitelisted IPs per day, warning email on 50th (or not, I dont care). Yes, conclusion in last sentence of your A is 100% correct - a few measures goes a long way and 'simple' becomes 'not so simple' before it becomes quite complex – Miloš Đakonović Jul 20 '16 at 5:42
  • Regarding "should define its PATH": isn't that already done by Defaults env_reset ? – Miloš Đakonović Jul 20 '16 at 7:29
  • @Miloshio the fail2ban tool, which has a goal opposite to yours, "times out" entries in its iptables chain. So after N minutes the entry is removed. Unfortunately this then adds yet more complexity... – roaima Jul 20 '16 at 7:32
  • @Miloshio, re PATH. Regardless of env_reset I'd want a security-sensitive script of mine to have a known PATH. – roaima Jul 20 '16 at 7:34
  • 1
    @roaima also, I'd draw your attention to my comment to the OP, you might want to mention either ipsets or CAP_NET_ADMIN; they don't warrant an answer on their own, but—CAP_NET_ADMIN especially is least-privilege, and ipsets has a simpler and probably more DoS-resistant interface. – derobert Jul 20 '16 at 16:26

One of the best thing is to use the "Digest_Spec" possibility in the sudoers file, to validate the checksum of your executable

Extract of the man page:

If a command name is prefixed with a Digest_Spec, the command will only match successfully if it can be verified using the specified SHA-2 digest.

Using openssl, to generate the checksum:

$ openssl dgst -sha224 /usr/local/sbin/mycommand

Then in your sudoers file (on the same line):

 www-data ALL=(ALL) NOPASSWD: 
| improve this answer | |
  • Thank you so much, I didn't know about this. Do we have to separate entries in multiple lines like in your example? – Miloš Đakonović Jul 21 '16 at 8:35
  • No, you must not, I wanted it to be readable – Adam Jul 21 '16 at 8:54

Named pipe approach. As root, run

mkfifo -m 666 /tmp/foo
/tmp/readpipe.sh &

And can, as user www-data then write to the pipe

echo test >>/tmp/foo

readpipe.sh in its simplest form (perl with taint would be better) :

while read A </tmp/foo
 echo received $A     
| improve this answer | |
  • 1
    I like your answer but I didn't understand why you recommended using pipe until I read comments. Also wouldn't mode 666 allow any user to read from or write to the pipe? – Melioratus Jul 18 '16 at 16:04
  • I don't see how this is materially different from sudo /usr/local/bin/someprogram "some argument". In both cases the root-based execution is protected by an interface. – roaima Jul 19 '16 at 21:32
  • 1
    @roaima - it's not protected - do a ps -ef and you'll see "some argument" clearly on display. If this were a password, it would be quite regrettable. And @Melioratus - yes, the 666 approach would need revising. – steve Jul 19 '16 at 21:55
  • You've defined an API that passes a string from your writer (let's call it Apache) and your reader (let's call it sudo mycommand). There's no way to access a privileged instance of mycommand except through this API, so this is your protected interface. (You can compare this to a kernel syscall interface.) – roaima Jul 19 '16 at 22:01
  • 1
    Agreed, the mode 666 part does need review. – steve Jul 21 '16 at 17:23

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.

The binary: 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), LC_* and 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. env_reset in 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 passwd, su, and 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).

| improve this answer | |
  • Thank you. I'll carefully examine your answer. Upvote so far. – Miloš Đakonović Jul 20 '16 at 14:33

What else can be done to secure a sudo-powered script: use PAM - pluggable authentication modules.

PAM administrators guide: http://www.linux-pam.org/Linux-PAM-html/Linux-PAM_SAG.html

PAM module writer's guide: http://www.linux-pam.org/Linux-PAM-html/Linux-PAM_MWG.html

PAM application developer's guide: http://www.linux-pam.org/Linux-PAM-html/Linux-PAM_ADG.html

also check /usr/share/doc/packages/pam/pdf on your system, for these documents which will be pertinent to the version of PAM on your system.

PAM is one of the fundamental security mechanisms in linux. It is used to secure and lockdown many things, two examples are the login process and password changing process. When you enforce password restrictions such as minimum character length and expiration, that is done with PAM. If PAM is good enough for core system processes, it is certainly good enough for your script. first look in the PAM System Administration Guide to get an idea of what you can already leverage, some examples:

  • pam_time: does not authenticate the user, but instead it restricts access to a system and or specific applications at various times of the day and on specific days or over various terminal lines. This module can be configured to deny access to (individual) users based on their name, the time of day, the day of week, the service they are applying for and their terminal from which they are making their request.

  • pam_env: cause the setting or unsetting of environment variables when invoked, use this to your advantage when creating your pam module

  • pam_localuser: help implementing site-wide login policies, where they typically include a subset of the network's users and a few accounts that are local to a particular workstation. Using pam_localuser and pam_wheel or pam_listfile is an effective way to restrict access to either local users and/or a subset of the network's user.

  • list goes on, refer to the sys admin's guide and use as needed.

you may also find this useful: http://freecode.com/projects/pam_script/

From here you will need to read the module writer's guide & app development guide. This is the purpose of PAM... pluggable authentication modules. You are effectively writing code, and if you are not already will become a programmer when you are done. This is not "IT" stuff where you click some check boxes enabled/disabled like in Microsoft windows. The reason for PAM is so the system admin can plug in a module (sometimes easy, sometimes not you have to write one) for security reasons to do whatever. For example a user using passwd to change his or her password, PAM is used around that for security and you don't type "sudo passwd" to change your password. Depending what your script needs to do, you might be not even need sudo if you use PAM properly and set ownership and SUID/GUID of your script, just like how /usr/bin/passwd is.

When you ask [yourself] what else can i do to secure this... might be an indication it's not the best way to do something. In this case using sudo to grant elevated privilege to run a script/executable. Instead look to established mechanisms of how linux already handles things like this... case in point user's use /usr/bin/passwd to edit the /etc/passwd and /etc/shadow files which are owned by root and need root permission to be modified. And with PAM, you can restrict who can run your script based on user id, group id, local user account, during a given time, and so on. Going further, pretty sure you can create a separate group account with it's own password, for sole purpose of running your script having only those users, and then you don't give out root password to run your script. You would be following the scheme of the wheel group for only those users who can su to root. hope this helps.

| improve this answer | |
  • if you bothered to read the pam admin guide: Quoting from the Linux-PAM System Administrator's Guide: "It is the purpose of the Linux-PAM project to separate the development of privilege granting software from the development of secure and appropriate authentication schemes. This is accomplished by providing a library of functions that an application may use to request that a user be authenticated." – ron Jul 20 '16 at 13:39
  • 1
    Flexibility is one of PAM's greatest strengths. PAM can be configured to deny certain programs the right to authenticate users, to only allow certain users to be authenticated, to warn when certain programs attempt to authenticate. I provided a link to an example of a home grown pam module that can be used to get started. This was to answer the question What else can be done. – ron Jul 20 '16 at 13:40
  • maybe when i see the -1 go back to a 0 for answer usefulness. PAM is easier said than done, and I do not know the extent of how you are looking to further "secure" your script. You can quickly go from practical to security theatre to problem. Refer to linux-pam.org/Linux-PAM-html/Linux-PAM_SAG.html. if you write a pam module for the script, you can use things like pam_limits, pam_shells, pam_localuser, pam_env, pam_exec, pam_time, and so on. The way PAM secures the user login process & the passwd changing process can be used for your script. If that's not good enough oh well – ron Jul 20 '16 at 19:08
  • 1
    Can you explain how this will help, given that the question was about securing a setuid executable, and your example of /usr/bin/passwd also involves using a setuid executable to run the PAM modules? – ilkkachu Jul 20 '16 at 21:17

Setting aside whether your approach is a good idea or not, if mycommand is interactive you should inhibit shell escapes using the noexec directive. This prevents the execution of other commands by presetting the exec() system calls to do nothing. (This is achieved through the mechanisms around LD_PRELOAD, see sudo.conf(5).) Caveat: this is not bulletproof - if mycommand is statically linked then this doesn't work.

| improve this answer | |

I will add this :

Dont make the script to be called directly, but to call a secondary script, the first script will be used to do log:


[ "$USER" != "www-data" ] && exit
set -a 
sudo realscript &>/var/log/myscript.log 

and the realscript:

[ "$USER" != "root" ] && exit 

In sudo, put the realscript as sudo permitted and not the called script as you are using.

And touch myscript.log as www-data owned, but 222 permissions (yes write only is possible!). Do the log backup in cron.

Dont allow www-data to see myscript contents, but just to execute it!

chmod 111 mylogscript
chmod 111 myrealscript

Permissions 111 is nice, because you dont want users reading the script but just executing it.

If is possible try to compile shell script:

shc mylogscript
shc myrealscript

Yes shc is a shell compiler so you can convert your script to a binary, and it will be more safe - but in some cases unstable, so be carefully!

Try to do not use parameters inside the shell script like $1 $2, this can be used to do script injection. Instead of that try to export environment variables , that shell can look at, or temp files. Parameter or not use all in some kind of openssl encryption (look to this post: How to make openssl encrypt passwords like php via command line

| improve this answer | |
  • logging from an unprivileged helper isn't that helpful since a malicious user could just run sudo realscript directly. also, I don't running shell scripts without read access (i.e. mode 111) will work since the shell actually has to read the file to interpret the commands. – ilkkachu Jul 22 '16 at 8:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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