Don’t use POSIX to create temporary files

Stop making huge security holes with POSIX tmpnam. You don’t need it in Perl because File::Temp, which comes with Perl, does it for you. Perl v5.22 deprecated tmpnam (and recommended replacements for tmpfile) and v5.26 has removed it.

The problem

This isn’t a change to Perl so much as a reaction in the Perl source to the recommendations of the underlying C toolkit. The POSIX tmpnam tries to create a filename that no one else is using and that you can safely use as a new file name. This is often used where you don’t want to keep the file but you need a place to store some data for a bit. Maybe you want to completely create a file before you move it into its final path (like the In-place editing gets safer in v5.28).

The problem is that there’s some time between the creation of the possible filename and its use. If another process creates that filename before you can, there’s a problem. That’s a “race condition” where you are competing with everything in the world to finish your process and hoping that nothing disturbs your assumptions.

The Open Web Application Security Project (OWASP) has a good summary of the issues in Insecure Temporary File. Safely Creating Temporary Files in Shell Scripts has interesting things to say.

Using a temp file

Instead of using POSIX, you can use File::Temp‘s tempfile. With no arguments it opens a file somewhere and returns the writable filehandle and filename in one shot:

use File::Temp qw(tempfile)
my($fh, $filename) = tempfile();

Here’s a run to show the crazy filename:

% perl -MFile::Temp=tempfile -le 'print +((tempfile())[1])'

You can set several options for tempfile:

  • DIR (string) – create the temporary file in this directory. The generated filename is catfiled with this path. Otherwise, it uses the value of TMPDIR.
  • TMPDIR (boolean) – use the operating system-specific value from File::Spec->tmpdir.
  • TEMPLATE (string) – a string template for the filename. There must be at least four trailing X characters which will be replaced by a generated substring.
  • SUFFIX (string) – add this suffix to the name (so the TEMPLATE can end in four or more Xs)
  • UNLINK (boolean) – if true, remove the file when nothing references it anymore.
  • EXLOCK (boolean) – if true, open the file with an exclusive lock

Here’s are the defaults (so you wouldn’t call it like this):

my( $fh, $filename ) = tempfile(
	DIR      =>  undef,     # where to put the file
	SUFFIX   =>  '',        # add this at the end of the name
	UNLINK   =>  0,         # clean-up the file at the end
	TMPDIR   =>  0,         # where to put the file
	EXLOCK   =>  0,         # exclusive lock
	TEMPLATE =>  'X' x 10,  # increment files with the same pattern

You might like to name your files after the program the produces it, add a suffix, and automaically remove it when it’s done:

use File::Basename;
my( $fh, $filename ) = tempfile(
	SUFFIX   =>  '.tmp',
	UNLINK   =>  1,
	TEMPLATE =>  basename($0) . '-XXXXXXXXXXX',

There’s a perlfaq How do I change, delete, or insert a line in a file, or append to the beginning of a file?. The basic answer is that you open a new file and write data to it in whatever order you want. That new file can then become the old file.

It’s important in these operations that you don’t replace the existing data until you know you’ve successfully created its replacement. You can do that in a temporary file first. After the temporary file is completely done you can move it into place of the original.

Here’s an example from the FAQ answer:

open my $in,  '<',  $file      or die "Can't read old file: $!";
open my $out, '>', "$" or die "Can't write new file: $!";
while( <$in> ) {
    print $out $_;
close $out;

Let File::Temp take care of all the details. Don’t remove the original data immediately when you are done, though; you want to move the original out of the way and move the new version into place. Something might go wrong at each step and you don’t want to lose the data anywhere along the way:

use File::Temp qw(tempfile);

open my $in,  '<',  $file or die "Can't read old file: $!";
my( $out, $tempfile ) = tempfile( UNLINK => 0 );

while( <$in> ) {
    print $out $_;
close $in;
close $out;

rename $file     => "$file.orig" or die "Could not move original";
rename $tempfile => $file        or die "Could not move new";
unlink "$file.orig"              or die "Could not remove original";

Things to remember