source: trunk/athena/bin/saferm/saferm.c @ 12031

Revision 12031, 10.5 KB checked in by ghudson, 26 years ago (diff)
Don't follow symlinks in safe_to_unlink().
Line 
1/* Copyright 1998 by the Massachusetts Institute of Technology.
2 *
3 * Permission to use, copy, modify, and distribute this
4 * software and its documentation for any purpose and without
5 * fee is hereby granted, provided that the above copyright
6 * notice appear in all copies and that both that copyright
7 * notice and this permission notice appear in supporting
8 * documentation, and that the name of M.I.T. not be used in
9 * advertising or publicity pertaining to distribution of the
10 * software without specific, written prior permission.
11 * M.I.T. makes no representations about the suitability of
12 * this software for any purpose.  It is provided "as is"
13 * without express or implied warranty.
14 */
15
16/*
17 * saferm
18 *
19 * Running as root, remove and optionally zero files in /tmp that
20 * may be owned by anybody, avoiding any possible race conditions
21 * resulting in security problems.
22 */
23
24static char rcsid[] = "$Id: saferm.c,v 1.4 1998-09-30 21:10:38 ghudson Exp $";
25
26#include <sys/types.h>
27#include <sys/stat.h>
28#include <fcntl.h>
29#include <string.h>
30#include <stdio.h>
31#include <unistd.h>
32#include <stdlib.h>
33#include <errno.h>
34
35char *program_name;
36
37#ifndef MIN
38#define MIN(a, b) ((a) < (b) ? (a) : (b))
39#endif
40
41/* Safely open a file with intent to zero. */
42int safe_open(char *filename, struct stat *file_stat)
43{
44  int file;
45  struct stat name_stat;
46
47  /* Try to open the file we think we want to zero. */
48  file = open(filename, O_WRONLY);
49  if (file == -1)
50    {
51      fprintf(stderr, "%s: open(%s) for writing failed: %s\n",
52              program_name, filename, strerror(errno));
53      return -1;
54    }
55
56  /* Stat the file we actually opened. */
57  if (fstat(file, file_stat) == -1)
58    {
59      fprintf(stderr, "%s: fstat(%s) failed: %s\n",
60              program_name, filename, strerror(errno));
61      close(file);
62      return -1;
63    }
64
65  /* Make sure it's a regular file. */
66  if (!S_ISREG(file_stat->st_mode))
67    {
68      fprintf(stderr, "%s: %s is %s\n",
69              program_name, filename,
70              (S_ISDIR(file_stat->st_mode)
71               ? "a directory"
72               : "not a regular file"));
73      close(file);
74      return -1;
75    }
76
77  /* Stat the file we think we want to zero. */
78  if (lstat(filename, &name_stat) == -1)
79    {
80      fprintf(stderr, "%s: lstat(%s) failed: %s\n",
81              program_name, filename, strerror(errno));
82      close(file);
83      return -1;
84    }
85
86  /* We must be sure that we are not zeroing a hard link or symbolic
87   * link to something like the password file. To do this, we must
88   * verify that the link count on the file we zero is equal to 1, and
89   * that we did not follow a symbolic link to reach that file.
90   *
91   * Simply lstat()ing the file before or after we open it is subject to
92   * lose in a race condition, as there is no way to know that the file we
93   * opened is the same file as the one we lstat()ed. Simply fstat()ing
94   * the file we actually open can't tell us whether we followed a symbolic
95   * link to get there, and also can't give us a reliable link count also
96   * due to race conditions.
97   *
98   * But if we both lstat() and fstat() and compare the inode numbers from
99   * the two cases, we can verify that the file we have open, at the time
100   * of our lstat(), existed in a unique location identified by the pathname
101   * we used to open it.
102   *
103   * So first, verify that our two stats got the same file.
104   */
105  if (file_stat->st_dev != name_stat.st_dev
106      || file_stat->st_ino != name_stat.st_ino)
107    {
108      fprintf(stderr, "%s: %s and file opened are not identical\n",
109              program_name, filename);
110      close(file);
111      return -1;
112    }
113
114  /* Now verify that the link count is 1. Note that file_stat may not be
115   * used here, otherwise we could be beaten by a race condition:
116   *
117   *   1. The attacker creates /tmp/passwd as a symlink to /etc/passwd.
118   *   2. We open /tmp/passwd, which follows the symlink, and so we have
119   *      fstat()ed /etc/passwd. The link count as of this stat is 1.
120   *   3. The attacker removes the /tmp/passwd symlink and replaces it
121   *      with a hard link to /etc/passwd.
122   *   4. We lstat() /tmp/passwd. The link count now is 2.
123   */
124  if (name_stat.st_nlink != 1)
125    {
126      fprintf(stderr, "%s: multiple links for file %s, not zeroing\n",
127              program_name, filename);
128      close(file);
129      return -1;
130    }
131
132  /* Now we know it is safe to muck with the file we opened. */
133  return file;
134}
135
136/* Zero the file represented by the passed file descriptor. Assumes
137 * that the file pointer is already at the beginning of the file, and
138 * file_stat points to a stat of the file.
139 */
140int zero(int file, struct stat *file_stat)
141{
142  char zeroes[BUFSIZ];
143  int written = 0, status;
144
145  memset(zeroes, 0, BUFSIZ);
146
147  while (written < file_stat->st_size)
148    {
149      status = write(file, zeroes, MIN(BUFSIZ, file_stat->st_size - written));
150      if (status == -1)
151        {
152          fprintf(stderr, "%s: write() failed: %s\n",
153                  program_name, strerror(errno));
154          return -1;
155        }
156      written += status;
157    }
158
159  fsync(file);
160  return 0;
161}
162
163/* Return non-zero if it is safe to unlink NAME. */
164int safe_to_unlink(char *name)
165{
166  struct stat st;
167 
168  if (lstat(name, &st) < 0)
169    {
170      fprintf(stderr, "%s: %s: %s\n", program_name, name, strerror(errno));
171      return 0;
172    }
173 
174  if (S_ISDIR(st.st_mode))
175    {
176      fprintf(stderr, "%s: %s is a directory\n", program_name, name);
177      return 0;
178    }
179
180  return 1;
181}
182
183/* path is the full pathname to a file. We cd into the directory
184 * containing the file, following no symbolic links. A pointer to just
185 * the filename is returned on success (a substring of path), and NULL
186 * is returned on failure.
187 */
188char *safe_cd(char *path)
189{
190  char *start_ptr, *end_ptr, *path_buf;
191  struct stat above, below;
192
193  if (path == NULL || *path == 0)
194    return NULL;
195
196  /* Make a copy of the path to be removed, since we're going to modify it. */
197  path_buf = malloc(strlen(path) + 1);
198  if (path_buf == NULL)
199    {
200      fprintf(stderr, "%s: out of memory\n", program_name);
201      return NULL;
202    }
203  strcpy(path_buf, path);
204
205  /* Step through each path component and cd into it, verifying
206   * that we wind up where we expect to.
207   *
208   * strchr()ing from start_ptr + 1 handles the absolute path case
209   * as well as beginning to address the double-/ case.
210   */
211  start_ptr = path_buf;
212  while (end_ptr = strchr(start_ptr + 1, '/'))
213    {
214      /* Include any remaining extra /'s at the end of this component. */
215      while (*(end_ptr + 1) == '/')
216        end_ptr++;
217
218      *end_ptr = 0;
219
220      if (lstat(start_ptr, &above) == -1)
221        {
222          fprintf(stderr, "%s: lstat of \"%s\" failed: %s\n", program_name,
223                  path_buf, strerror(errno));
224          free(path_buf);
225          return NULL;
226        }
227
228      if (!S_ISDIR(above.st_mode))
229        {
230          fprintf(stderr, "%s: final component of \"%s\" is not a directory\n",
231                  program_name, path_buf);
232          free(path_buf);
233          return NULL;
234        }
235
236      if (chdir(start_ptr) == -1)
237        {
238          fprintf(stderr, "%s: chdir to \"%s\" failed: %s\n", program_name,
239                  path_buf, strerror(errno));
240          free(path_buf);
241          return NULL;
242        }
243
244      if (stat(".", &below) == -1)
245        {
246          fprintf(stderr, "%s: stat of \"%s\" failed: %s\n", program_name,
247                  path_buf, strerror(errno));
248          free(path_buf);
249          return NULL;
250        }
251
252      if (above.st_dev != below.st_dev || above.st_ino != below.st_ino)
253        {
254          fprintf(stderr, "%s: final component of \"%s\" changed during "
255                  "traversal\n", program_name, path_buf);
256          free(path_buf);
257          return NULL;
258        }
259
260      /* Advance to next component. */
261      start_ptr = end_ptr + 1;
262
263      /* Restore the / we nulled for diagnostics. */
264      *end_ptr = '/';
265    }
266
267  free(path_buf);
268  return (start_ptr - path_buf) + path;
269}
270
271/* Safely remove and optionally zero a file. -1 is returned for any
272 * level of failure, such as zeroing failed but unlinking succeeded.
273 */
274int safe_rm(char *path, int zero_file)
275{
276  int file, status = 0;
277  struct stat file_stat;
278  char *name;
279
280  name = safe_cd(path);
281  if (name == NULL)
282    return -1;
283
284  /* Do the unlink before the actual zeroing so that the appearance
285   * is atomic.
286   */
287
288  if (zero_file)
289    {
290      file = safe_open(name, &file_stat);
291      if (file == -1)
292        status = -1;
293    }
294
295  if (safe_to_unlink(name) && unlink(name) == -1)
296    {
297      fprintf(stderr, "%s: error unlinking %s: %s\n", program_name,
298              path, strerror(errno));
299      status = -1;
300    }
301
302  if (zero_file && file != -1)
303    {
304      if (zero(file, &file_stat) == -1)
305        status = -1;
306      close(file);
307    }
308
309  return status;
310}
311
312/* Safely remove a directory. -1 is returned for any level of failure. */
313int safe_rmdir(char *path, int quietflag)
314{
315  char *name;
316
317  name = safe_cd(path);
318  if (name == NULL)
319    return -1;
320
321  if (rmdir(name) == -1)
322    {
323      /* System V systems return EEXIST when a directory isn't empty
324       * but hack the rmdir command to display a better error message.
325       * Well, we can do that too.
326       */
327      if (errno == EEXIST)
328        errno = ENOTEMPTY;
329
330      if (!quietflag || errno != ENOTEMPTY)
331        {
332          fprintf(stderr, "%s: error removing directory %s: %s\n",
333                  program_name, path, strerror(errno));
334        }
335      return -1;
336    }
337
338  return 0;
339}
340
341int main(int argc, char **argv)
342{
343  int c, zeroflag = 0, dirflag = 0, quietflag = 0, errflag = 0;
344  int dir, status = 0;
345
346  /* Set up our program name. */
347  if (argv[0] != NULL)
348    {
349      program_name = strrchr(argv[0], '/');
350      if (program_name != NULL)
351        program_name++;
352      else
353        program_name = argv[0];
354    }
355  else
356    program_name = "saferm";
357
358  while ((c = getopt(argc, argv, "dqz")) != EOF)
359    {
360      switch(c)
361        {
362        case 'd':
363          dirflag = 1;
364          break;
365        case 'q':
366          quietflag = 1;
367          break;
368        case 'z':
369          zeroflag = 1;
370          break;
371        case '?':
372          errflag = 1;
373          break;
374        }
375    }
376
377  if (errflag || optind == argc || (dirflag && zeroflag))
378    {
379      fprintf(stderr, "usage: %s [-q] [-d|-z] filename [filename ...]\n",
380              program_name);
381      exit(1);
382    }
383
384  /* Remember our current directory in case we are being passed
385   * relative paths.
386   */
387  dir = open(".", O_RDONLY);
388  if (dir == -1)
389    {
390      fprintf(stderr, "%s: error opening current directory: %s\n",
391              program_name, strerror(errno));
392      exit(1);
393    }
394
395  while (optind < argc)
396    {
397      /* If we're removing a relative path, be sure to cd to where
398       * we started.
399       */
400      if (argv[optind][0] != '/')
401        {
402          if (fchdir(dir) == -1)
403            {
404              fprintf(stderr, "%s: error cding to initial directory: %s\n",
405                      program_name, strerror(errno));
406              exit(1);
407            }
408        }
409
410      if (dirflag && safe_rmdir(argv[optind], quietflag) == -1)
411        status = 1;
412      else if (!dirflag && safe_rm(argv[optind], zeroflag) == -1)
413        status = 1;
414
415      optind++;
416    }
417
418  exit(status);
419}
Note: See TracBrowser for help on using the repository browser.