1

I want to safely write to a destination file (as root, unter common Linux'es) with "echo" (or catany other Bash built-in stuff) like this

echo "foo" > /destination/dir/filename

But the problem is that /destination/dir could be accessible for normal system users, so there is the risk of symlink conditions.

I read all the "how to" for preventing TOC-TOU stuff when using C, so NOT checking for symlink/remove it and open then (the common recommendations seems to be to open() with O_NOFOLLOW).

But all of this (access to kernel open() and it's flags) is not possible via Bash (or am I wrong?).

Then I got the idea of

  • creating a tempfile with mktemp
  • chown+chmod the tempfile appropriately
  • write the contents to write to the tempfile
  • move the tempfile to the destination dir with the Bash param "-T"

So as some Bash peudo-code (without error checking at some places)

TEMPFILE=$(mktemp)

chown root:root $TEMPFILE chmod 0600 $TEMPFILE echo "contents" > $TEMPFILE mv -T $TEMPFILE /destination/dir/filename

I just tested it with "/destination/dir/filename" to be a symlink to a system file, but it worked: "mv" did move the tempfile correctly to the "filename", the symlink was removed (which is was I intended), no file was overwritten.

Is there anythink I missed out with regards to security/race conditions etc.?

Thanks :-)

  • Check if /destination/dir/filename exists, remove it and then echo "foo" > /destination/dir/filename? – Cyrus Jun 18 '22 at 20:52
  • @Cyrus you still have a race condition there - it's just that now it's between the "remove it" and the echo "foo" – Chris Davies Jun 18 '22 at 20:59

1 Answers1

1

Let's assume that /destination/dir exists safely. (If it does not, then create the topmost directory with sufficiently restricted permissions that its subdirectories cannot be accessed or created by a non-root user. Then relax the permissions once the hierarchy is complete.)

One approach is to create the file with a temporary but unique name using mktemp inside the target directory. Then write its contents, and finally mv it to the destination.

The key point is that when using mv with a source and destination on the same filesystem the destination will be removed as part of the rename process: it's an atomic operation performed through the rename(2) system call by the kernel itself:

If newpath already exists, it will be atomically replaced, so that there is no point at which another process attempting to access newpath will find it missing. However, there will probably be a window in which both oldpath and newpath refer to the file being renamed.

A simple implementation, very similar to your own, might be this:

base='/destination/dir'                # Use '.' for current directory
file='filename'

tf=$(mktemp "$base/XXXXXXXXXX.tmp") # Created with mode 600 echo "contents" >"$tf" # Always double-quote variables when used if ! mv -Tf "$tf" "$base/$file" then echo "Error writing to $base/$file" >&2 rm -f "$tf" # Clean up temporary file fi

In a POSIX world, implementing an equivalent to mv -T is harder, and here I've relied on being able to create a temporary directory. In a real situation this would probably be best handled in a loop that repeats mkdir with different directory names until it succeeds, but here I only attempt the creation once:

base='/destination/dir'                # Use '.' for current directory
file='filename'

td="$base/dir.$$.tmp" # Must be unique if mkdir -m700 "$td" # Will fail if not unique then echo "contents" >"$td/$file" if ! mv -f "$td/$file" "$base/" # Overwrite/replace $base/filename, or fail then echo "Error writing to $base/$file" >&2 fi rm -rf "$td" # Clean up temporary directory else echo "Error creating temporary directory $td" >&2 fi

The mktemp command is also not POSIX, but there are suggested implementations available for that.

Chris Davies
  • 116,213
  • 16
  • 160
  • 287