3

The problem: I have a script that runs periodically via a cron job as root, but I want to give people a way to kick it off asynchronously too, via a webpage. (The script will be written to ensure it doesn't run overlapping instances or such.)

I don't need the users to log in or have an account, they simply click a button and if the script is ready to be run it'll run. The users may select arguments for the script (heavily filtered as inputs) but for simplicity we'll say they just have the button to choose to press.

As a simple test, I've created a Python script in cgi-bin. chown-ing it to root:root and then applying "chmod ug+" to it didn't have the desired results: it still thinks it has the effective group of the web server account... from what I can tell this isn't allowed.

I read that wrapping it with a compiled cgi program would do the job, so I created a C wrapper that calls my script (its permissions restored to normal) and gave the executable the root permissions and setuid bit. That worked... the script ran as if root ran it.

My main question is, is this normal (the need for the binary wrapper to get the job done) and is this the secure way to do this? It's not world-facing but still, I'd like to learn best practices.

More broadly, I often wonder why a compiled binary is more "trusted" than a script in practice? I'd think you'd trust a file that was human-readable over a cryptic binaryy. If an attacker can edit a file then you're already in trouble, more so if it's one you can't easily examine. In short, I'd expect it to be the other way 'round on that basis. Your thoughts?

MartyMacGyver
  • 167
  • 3
  • 11

3 Answers3

6

Another answer, since this is an entirely different (and much more generic) approach. One could argue that this is the canonical way to do such things :)

Put your apache user (however it is named, I assume www-data) into the sudoers file (with visudo) with a very specific line that allows him to only run yourscript.py and only from the local machine (hostname), without a password. Something like this:

www-data hostname=(root)NOPASSWD:/path/to/yourscript.py

Your CGI script can then start this script with a call to sudo /path/to/yourscript.py.

Of course, you have to make extra sure that parameters you might use for this script are totally restricted to allowed values - optimally from both the cgi wrapper script and yourscript.py.

Sven
  • 97,248
  • 13
  • 177
  • 225
  • The funny thing is, if I slap a compiled wrapper in there that calls the script it all works fine already. If I just directly use the script (with the permissions set the way the wrapper was) it fails. If the worry is about scripts in general then allowing one is as bad as allowing any - but if binaries get preferential treatment then it seems like just another version of the same security "hole". – MartyMacGyver Oct 27 '12 at 17:33
3

The IMHO easiest (and quite secure) way is an indirect one: Make the webpage create a file somewhere if the user clicks the button and modify your script to only run if the file exists and delete it afterwards. Then run this script from a cron job every minute. You can even use this file to supply arguments to the script (which of course needs special attention).

This leaves room for improvement (concurrent clicks by multiple users being one) but I have used this approach for clean-up and one-time action purposes many times.

Sven
  • 97,248
  • 13
  • 177
  • 225
  • 1
    It's a thought, but I prefer a more direct effect: I don't want the user to have to wait at all for the process to begin. I may eventually display the progress (or a summary thereof) live on the page as well. Most control-panel type things I've seen do not seem to have cron jobs spinning in the background waiting for something to happen. – MartyMacGyver Oct 27 '12 at 09:16
3

First of all: don't do this, or at least do not allow any parameters to be passed from the web to your root-running application.

Now to your actual question: When you write a script (Python in your case), it gets executed by the interpreter. So to be able to run the script as root, you would need to set the python interpreter suid-root, but you really do not want to do this. This is because your script isn't an executable as such, but just a set of rules for the interpreter. When you wrap your script with a binary executable, you now again have an executable that gains root rights and the python interpreter called from the executable has root too. More information on this can be found in Cannot Set UID on Shell Scripts and http://www.diablotin.com/librairie/networking/puis/ch05_05.htm

That said, calling the script via sudo as @SvenW suggested should work fine.

zhenech
  • 1,492
  • 9
  • 13
  • Well, that explains why setuid on a script doesn't do what I thought it would and why the wrapper does (I should've realized that...) Still doesn't quite explain why in general scripts seems to be frowned upon when executed by root, but perhaps it was this subset of cases only. As for parameters, if any are passed in they'll be indirect/filtered (I can foresee a limited set of pre-conditioned inputs that'd be further conditioned by the appropriate arg parser for the scripting language). But I'll definitely keep that in mind, particularly for things that might face externally. – MartyMacGyver Oct 28 '12 at 10:16
  • I wouldn't say scripts are frowned upon, but letting your webserver run *anything* as root. I know there are usecases where you have to do this, but you should think about security ten times (and not only twice as usual) when doing so. This applies to binaries in the same way as scripts: a binary with an exploitable bufferoverflow un as root is as bad as a script, so there should be really no difference here. – zhenech Oct 28 '12 at 16:12