Check then use is a dangerous security pattern

December 14, 2007

A commentator here recently noted the danger of the Unix pattern of stat()'ing a filename, checking to see if the stat results look good, and then open()'ing the file. The problem is that in many cases an attacker can change what the filename is pointing to between the time you stat() it and when you open() it, so that you check a harmless thing but open a dangerous thing.

In thinking about this, I came to realize that this is an example of general security problem pattern: checking something and then using it. If an attacker can figure out how to mutate the thing between your check and your use of it, you have a security vulnerability. You either need atomic 'check and use' operations, or you need to change to a 'capture and check' pattern instead, where you make your checks only after you have captured an immutable reference to the real object you will be working on.

(This also implies that you need some safe way of getting an immutable reference. Unfortunately open() does not quite qualify on most Unixes, because there is no way to tell it to refuse to do opens that would have side effects, such as opening some device files.)

It doesn't matter if you can't see a way to mutate the thing between your check and the actual use. First, attackers have proven they are very ingenious, and second, even if there is no way now, later changes to the overall system may introduce one, because you are ultimately counting on behavior that is not guaranteed.


Comments on this page:

From 194.8.197.205 at 2007-12-15 00:18:34:

You can generalise further than that, to the point of a tautology: “non-atomic operations + multitasking = race condition.”

Aristotle Pagaltzis

By cks at 2007-12-15 14:12:40:

Sure, but partly my hope is that this particular pattern is specific enough that I'll recognize it in my code (in more than the stat() and then open() example) or in code that I'm looking at.

From 83.145.204.27 at 2007-12-16 01:51:03:

So for the classic race between open() and stat(), should we try to construct something on the lines of a four-way "check, capture, check, and then use"? More specifically, I was thinking something like:

1. stat() or lstat() a file with appropriate checks (if not S_ISREG, reject, and so on) and save the device ID together with the inode number of the file.

2. open() the file (if writing Linux-specific code, maybe with additional flags such as O_NOFOLLOW).

3. fstat() the file descriptor; do the checks again and compare the saved statistics - proceed only if a match is found.

4. Use the file descriptor.

The struct stat provides many other checks that can be made (e.g. the number of hardlinks), but that the above procedure sure sounds a little too excessive, at least if such a call would be made often. In this particular example something like IS_A_REGULAR_FILE -flag for open() in libc/kernel would make us all happy.

In any case, the low-level details that are exposed by C pose an interesting question for higher level languages that was the context of the original post. So if I write, say, Python in Unix environments, do I only have a yes/no choice for open() or file(): either I trust such calls or I do not?

Finally, as cks commneted, the open()/stat() race is a crude example known for decades; the real danger may lie in equivalent conditions that are not so obvious for our eyes.

By cks at 2007-12-16 23:22:24:

I think that if you are seriously worried about opening device files, you need to do the open() itself as an unprivileged user and then pass the opened file descriptor back to the main process over Unix domain sockets (and then inspect it with fstat().). Nothing else really protects you.

Things are not totally hopeless in high-level languages like Python that expose direct access to file descriptors; you can still do the open() then fstat() dance in some form. On the positive side, you are generally not writing setuid programs in Python or other high level languages so you tend not to have these particular problems.

By Dan.Astoorian at 2007-12-17 10:46:52:

I think that if you are seriously worried about opening device files, you need to do the open() itself as an unprivileged user and then pass the opened file descriptor back to the main process over Unix domain sockets

Assuming the main process is running as root, you can also seteuid(n) to the unprivileged user for the open(), then seteuid(0) back again if necessary, rather than pass file descriptors around.

The details are surprisingly tricky to get right (e.g., a suid program may need to set its real uid to 0 in order to be able to seteuid(0) back to root; you must check return values religiously; and you probably also want to clear out the supplementary groups and set the egid in addition to the euid), but it's doable.

On the positive side, you are generally not writing setuid programs in Python or other high level languages so you tend not to have these particular problems.

Not necessarily; I've written maintenance scripts which are run by root but which are designed to clean up problems under users' home directories (e.g., to correct inappropriate permissions on their dot-files). For what it's worth, in such cases, it's way, way easier to seteuid() to the user on whose behalf you're doing the work than to do the whole dance to make sure that ~/.Xauthority isn't a link to somewhere evil when you go to chmod() it.

--Dan

From 83.145.204.27 at 2007-12-20 11:35:57:

>"Assuming the main process is running as root, you can also seteuid() to the unprivileged user for the open(), then seteuid(0) back again if necessary, rather than pass file descriptors around."

Well this has always been a possibility, but many, me among them, consider euid() somewhat poisonous, and certainly worse than passing file descriptors. Better than nothing of course, if done carefully, but generally when you are dropping (root) priviliges, you should drop the real UID, once and for all.

For these reasons, cks' comment about passing the descriptors via unix domain sockets sounds more interesting. Generally: the so-called "privilige separation" (cf. e.g. OpenSSH) sounds more fruitful than playing with the effective user IDs, given that you need to retain the root priviliges for reason or another.

Written on 14 December 2007.
« Doing one-shot booting with GRUB
There are reasons for stupid anti-spam policies »

Page tools: View Source, View Normal, Add Comment.
Search:
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Fri Dec 14 23:17:00 2007
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.