Beefy Boxes and Bandwidth Generously Provided by pair Networks
laziness, impatience, and hubris

opening files: link checking and race conditions

by danderson (Beadle)
on Aug 02, 2005 at 23:59 UTC ( #480321=perlquestion: print w/replies, xml ) Need Help??

danderson has asked for the wisdom of the Perl Monks concerning the following question:

Hello most esteemed monks,

I have a most harrowing situation. I need to write a suid script that appends to a file. It is not suid root or anything, but I don't want to give anyone access they don't deserve. Problematically, -l doesn't work on filehandles, which kind of makes sense I suppose.

So here's the problem: I can't use
open(">>foo") unless (-l "foo")
since there's a race condition between the check and the open. I can't open the file, and check to see if it's a link in retrospect, since one cannot check for linkness on filehandles, and if I just checked by the filename, then there's just a different race case.

I've SuperSearched, I've googled, I've asked a perl goddess, and despite the fact that I'm sure there's an easy solution I can't find it.

So: is there a way to ease my troubled soul? Can my script safely open files, knowing in it's heart of hearts that they are not links? Even if the answer is "no," it would at least quiet my tempestuous conscience.

Replies are listed 'Best First'.
Re: opening files: link checking and race conditions
by graff (Chancellor) on Aug 03, 2005 at 01:15 UTC
    correction: this reply, though maybe not totally stupid on its own, is irrelevant to the OP (thanks to dave_the_m for waking me up.)

    I adapted some code from an old Perl Journal article by Sean Burke, and I've been using it for years; I posted it here a while back, in response to a similar SoPW thread.

    The basic idea is called a "semaphore" file -- a separate file from the one you intend to modify, whose sole purpose is to be the object of an flock operation. Since you aren't going to modify the semaphore file in any way, you side-step the race condition issue. Once one process owns the semaphore, it can do what ever number of steps it needs to in an "atomic" fashion, then free the semaphore for some other process.

    Hope that helps.

    (update: I just noticed that the url embedded in my posted "" code is no longer usable -- 404. TPJ is still available online, and you can get the article reference by searching for "semaphore file Sean Burke", but you have to pay for a subscription to TPJ to see it. Bear in mind that the TPJ subscription is definitely a good investment. Apologies for the broken link.)

      That's only useful for cooperating processes. The OP is talking about a malicious attempt to exploit a race condition.

      The only thing I can think of is to use O_NOFOLLOW, which isn't portable (but should work on Linux and BSD systems):

      use Fcntl; sysopen F, "/tmp/foo", O_RDONLY|O_NOFOLLOW;


Re: opening files: link checking and race conditions
by Tanktalus (Canon) on Aug 03, 2005 at 03:35 UTC

    Side-note: I have a similar "run as this other (non-root) user" case which is solved very nicely with sudo - anyone can run the script, but if the script detects it's run by the wrong user, it sudo's itself. And then I can give everyone appropriate access to run it with or without a password as required.

    Besides, I'm not entirely clear on why writing via a symlink is a bad idea. Generally speaking, it's a very good idea to ignore the fact that a file is really a symlink to somewhere else. Lots of really cool unixy things happen when you don't worry about symlinks. You may have a special case, but the chances of that is low - that's why it's a special case. ;-)

Re: opening files: link checking and race conditions
by graff (Chancellor) on Aug 03, 2005 at 04:32 UTC
    Well here's a chance for me to learn something... Now that I know my first reply missed the mark completely, I'm curious to find out:

    Suppose you do something like this:

    unless ( -l "foo" ) { open( FH, ">>foo" ) or die "foo: $!"; } die "Link attack detected" if ( -l "foo" );

    How does that fail to provide the protection that your script, in its heart of hearts, really wants to provide? I realize that if the symlink gets created during that very brief period of vulnerability, and happens to point to a non-existent file in a valid directory path (with write permission for the effective uid), then a new file will be created according to the name that the abuser has assigned as the symlink's target.

    So (sorry, I am honestly naive here) what? Except for that one scenario, it seems to me that no situation arises where any change is made to any file or directory. If the malicious symlink points to an existing data file, the script will die before actually altering that file; if the symlink points to a non-existant directory, the open call itself would fail; creating the symlink too soon will be trapped by the "unless ( -l )", and trying to create too late will fail.

    Just in that one specific scenario, a zero-length file could be created, owned by the effective uid of the script -- but nothing will be written to it, the script dies, and whoever was trying the exploit would need a different suid tool to put any data in that file (assuming a proper umask was in place when your script ran).

    What am I missing?

Re: opening files: link checking and race conditions
by dave_the_m (Monsignor) on Aug 03, 2005 at 09:40 UTC
    A general comment on various posts above. People just don't seem to be getting what a race condition is. Imagine I run the following in the background:
    while (1) { symlink '/tmp/foo', '/home/user/.rhosts'; unlink '/tmp/foo'; }
    Then when I run the OP's suid script, there's a good chance that between the test and the open, the symlink may appear; thus I get to create or truncate any file owned by that user. Even checking afterwards that the just-opened file is not a link wont necessarily work.


      dave_the_m: O_NOFOLLOW is a great start. Since, as a rule, suid/guid only matters on *nix scripts, and in fact this one is designed specifically for *nix (it uses /home/username in it) that'll do nicely. I'll put a warning in the header to ensure that it's only run on systems that correctly implement O_NOFOLLOW; I'm suprised I hadn't seen that flag in my wanderings. Thanks!

      Tanktalus: sudo doesn't help; I don't care who's running it, in fact anyone should be able to (that's the point of suid, right? Run as one user when you're really another). Also, writing via a symlink /is/ often a Very Bad Idea; see the example in dave_the_m's reply below. If the user is suid root, then you've appended to rhosts, and suddenly there are people being trusted who shouldn't be.

      graff, sk: I know it looks that simple, but bear in mind that processes run "concurrently" (not really, unless you're on a multiprocessor system, but they fake it). So looking at dave_the_m's example, all you need is for the timing to be just right. Since as a rule a malicious attacker doesn't mind running something (probably nice'd) a few thousand times to get an attack to work, testing before and after isn't enough. Google "atomicity" for more info.

      So: a general solution? Seems like no. One that will work for this situation? Yes. Suid isn't common on most systems that don't support the O_NOFOLLOW flag (believe it or not, suid exists to a degree on windows - eg. run 'at' as admin, have it open cmd.exe - bam you've escalated to Machine and can overwrite/modify/delete absolutely anything), so hopefully my incessant curiosity won't spend any more of my time on the topic, until it's a neccessary question again.

      Thanks for all your replies, I really appreciate them! Oh monks, you have yet again shown this follower a better path.
      Then when I run the OP's suid script, there's a good chance that between the test and the open, the symlink may appear; thus I get to create or truncate any file owned by that user. (emphasis added)

      But,  open( O, ">>foo" ) does not truncate the output file (and dying before writing leaves it unmodified). So why would there still be a problem with the code suggested in my second reply?

        Because you're still vulnerable to the race condition, it's just very timing dependent. Here's how the timeline falls out. At first, the file doesn't exist.
        unless ( -l "foo" ) { # we get here fine #*now* the link is created open( FH, ">>foo" ) or die "foo: $!"; #and now it's removed again } die "Link attack detected" if ( -l "foo" ); #too late, the link is gone already, so we didn't die.
        Now, it's really, really hard to get the timing to work perfectly for that, but that doesn't mean that it won't happen sometimes. So if you start a program creating and removing the link very quickly, and at the same time run the other program again and again, sooner or later you're going to get unlucky.
Re: opening files: link checking and race conditions
by sk (Curate) on Aug 03, 2005 at 05:14 UTC
    I agree with graff! Even the zero byte file can be take care of. You can first check for "exists" on the file. If it exists you will use >> otherwise you can open using >. You can also set a flag if you end up creating a new file.

    In the next check for the link after open in graff's code you can not only die on link but check for it and then cleanup if your flag was set to "create" and not append!

    open - to my knowledge does not execute anything so I guess that shouldn't cause concerns. I am not sure whether i am missing something very critical here!

    I agree, you certainly need to think through security issues before you allow other people to run your script.


Re: opening files: link checking and race conditions
by (anonymized user) (Curate) on Aug 03, 2005 at 12:40 UTC
    I hope I am not being dense, but I can't yet see why you can't check for linkness on the file using it's spec but after opening.
    my $lflag; open my $fh, ">>foo" or ErrorHandler( $?, $! ); $lflag = ( -l 'foo' ); unless( $lflag } { # process file } close $fh; # it was opened for append but append only occurred for non +-link case

    One world, one people

        okay but if that is a problem too, doesn't this fix both?
        my $lflag = ( -l 'foo' ); # check before opening $lflag or open my $fh, ">>foo" or ErrorHandler( $?, $! ); $lflag = ( -l 'foo' ); # and again after opening unless( $lflag } { # process file } $lflag or close $fh;

        One world, one people

Log In?

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://480321]
Approved by Enlil
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others about the Monastery: (14)
As of 2019-10-17 15:51 GMT
Find Nodes?
    Voting Booth?