/*
 * safopen.c - hopefully secure wrappper for fopen()
 * $Id: safopen.c,v 1.5 2005/05/10 13:49:17 rdenisc Exp $
 */

/***********************************************************************
 *  Copyright (C) 2002-2004 Remi Denis-Courmont.                       *
 *  This program is free software; you can redistribute and/or modify  *
 *  it under the terms of the GNU General Public License as published  *
 *  by the Free Software Foundation; version 2 of the license.         *
 *                                                                     *
 *  This program is distributed in the hope that it will be useful,    *
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of     *
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               *
 *  See the GNU General Public License for more details.               *
 *                                                                     *
 *  You should have received a copy of the GNU General Public License  *
 *  along with this program; if not, you can get it from:              *
 *  http://www.gnu.org/copyleft/gpl.html                               *
 ***********************************************************************/

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <stdio.h>
#include <string.h> /* memset(), memcmp() */
#include <fcntl.h> /* open() */
#include <sys/stat.h> /* lstat(), fstat() */
#include <unistd.h> /* close(), lseek() */
#include <errno.h>

#include "secstdio.h"

#ifdef WINSOCK
FILE *
secure_fopen (const char *path, const char *mode)
{
	return fopen (path, mode);
}
#else

# ifndef O_NOFOLLOW
#  define O_NOFOLLOW 0 /* only *BSD & Linux and maybe few others have that */
# endif

# ifndef O_LARGEFILE
#  define O_LARGEFILE 0
# endif

#if (defined (HAVE_STAT) && !defined (HAVE_LSTAT))
# define lstat( filename, structure ) stat (filename, structure)
/* lstat() is not defined by POSIX. We use stat() instead. */
# define HAVE_LSTAT 1
#endif

/*
 * As secure as possible replacement fopen(path, "a").
 * This function is used to open all log files (excepted stdout).
 *
 * DO submit patches against this function if you think it is still
 * insecure, and have other ideas.
 *
 * I currently assume that the situation is as follow:
 *
 * # Read-only open mode is secure.
 *
 * # Append open mode (whether read is enabled or not) is secure as long as
 * the file is regular or does not exists. We need lstat() and fstat().
 * To prevent a race condition, we check the file after it has been opened.
 *
 * # Write open mode, which causes the file to be erased automatically, is
 * never absolutely secure. The file has to be regular or not to exists.
 * To minimize race condition risks, we need O_NOFOLLOW (but this does not
 * protect from non link, non regular files that we may open, for example,
 * FIFOs).
 *
 * It is up to the caller to set adequate eUID, eGID and umask before calling
 * this function.
 */
FILE *
secure_fopen (const char *path, const char *mode)
{
	int fd, flags;
	const char *modep;
	struct
	{
		unsigned readf:1;
		unsigned writf:1;
		unsigned creaf:1;
		unsigned trunf:1;
		unsigned appef:1;
	} opts = { 0, 0, 0, 0, 0 };
	FILE *stream;
#ifdef HAVE_LSTAT
	struct stat st1, st2;
#endif

	/* Digests open mode */
	for (modep = mode; *modep; modep++)
		switch (*modep)
		{
			case 'r':
				opts.readf = 1;
				break;
				
			case 'w':
				opts.writf = opts.creaf = opts.trunf = 1;
				break;
				
			case 'a':
				opts.writf = opts.creaf = opts.appef = 1;
				break;
				
			case '+':
				opts.readf = opts.writf = 1;
				break;
		}

	if (opts.readf)
		flags = (opts.writf) ? O_RDWR : O_RDONLY;
	else
	{
		if (opts.writf)
			flags = O_WRONLY;
		else
		{
			errno = EACCES;
			return NULL; /* don't read, nor write! */
		}
	}

	if (opts.trunf)
		flags |= O_TRUNC;

#ifdef HAVE_LSTAT
	/* Gets current file state */
	memset (&st1, 0, sizeof (st1));
	if (lstat (path, &st1))
	{
		if ((errno != ENOENT) || !opts.creaf)
			return NULL;

		flags |= O_CREAT|O_EXCL;
		/*
		 * The file did NOT exists at the time of lstat().
		 * O_CREAT will create it, while O_EXCL will prevent a race
		 * condition if it is created between lstat() and open() by
		 * another concurrent process.
		 */
	}
	else	/* The file already exists. We don't want to create it. */
	{
		opts.creaf = 0;
		if (opts.writf)
		/*
		 * If the file is to be opened in write mode, we have some
		 * safety checks:
		 * 
		 * Refuses to open a non-regular file. This includes:
		 * # links: might be used to modify files holding important
		 *   data
		 * # FIFO, some devices: may block output to log file and I
		 *   know no legitimate use them anyway.
		 * # sockets, directories, other devices: usually cannot be
		 *   opened or used properly anyway.
		 *
		 * Refuses to open hard-linked file for the same reason as
		 * links.
		 */
		{
			if ((st1.st_nlink != 1) || !S_ISREG (st1.st_mode))
			{
				errno = EPERM;
				return NULL;
			}
			flags |= O_NOFOLLOW;
		}
	}
#else
	if (opts.creaf)
		flags |= O_CREAT;
#endif

	fd = open (path, flags | O_LARGEFILE, 0666);
	if (fd == -1)
		return NULL;
	
#ifdef HAVE_LSTAT
	if (!opts.creaf)
	{
		/*
		 * Now, we have to make sure the file was not changed
		 * between lstat() and open().
		 */ 
		memset (&st2, 0, sizeof (st2));
		if (fstat (fd, &st2)
		 || memcmp (&st1, &st2, sizeof (struct stat)))
		{
			close (fd);
			errno = EPERM;
			return NULL;
		}
	}
#endif
	/* Goes to end of file if needed */
	if (opts.appef && (lseek (fd, 0, SEEK_END) == -1))
	{
		close (fd);
		return NULL;
	}
	
	stream = fdopen (fd, mode);
	if (stream == NULL)
		close (fd);
	return stream;
}

#endif /* WINSOCK */


syntax highlighted by Code2HTML, v. 0.9.1