[alt.sources] Beware xargs security holes

brnstnd@kramden.acf.nyu.edu (Dan Bernstein) (10/09/90)

Yeah. xargs should have a -0 option for taking null-separated filenames
for its input. find should have a -print0 option for producing similar
output.

I wrote a somewhat improved PD ``apply'' clone out of boredom. It can
execute commands itself, pass them to sh, or print them for sh batching.
At least it's reasonably secure; you can feel safe with apply ... "$@".
Copy enclosed. Kids, don't use this awful coding style at home.

---Dan

#! /bin/sh
# This is a shell archive.  Remove anything before this line, then unpack
# it by saving it into a file and typing "sh file".  To overwrite existing
# files, type "sh file -c".  You can also feed this as standard input via
# unshar, or by typing "sh <file", e.g..  If this archive is complete, you
# will see the following message at the end:
#		"End of shell archive."
# Contents:  Makefile apply.c getopt.h apply.1
# Wrapped by brnstnd@kramden on Sun Sep  9 05:33:06 1990
PATH=/bin:/usr/bin:/usr/ucb ; export PATH
if test -f 'Makefile' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'Makefile'\"
else
echo shar: Extracting \"'Makefile'\" \(415 characters\)
sed "s/^X//" >'Makefile' <<'END_OF_FILE'
XCC=cc
XCCOPTS=-O4 -s -DVFORK
X
XNROFF=nroff
XNROFFOPTS=-man
X
Xall: apply apply.man
X
Xshar: apply.shar
X
Xapply: apply.o Makefile
X	$(CC) $(CCOPTS) -o apply apply.o
X
Xapply.o: apply.c getopt.h Makefile
X	$(CC) $(CCOPTS) -c apply.c
X
Xapply.man: apply.1 Makefile
X	$(NROFF) $(NROFFOPTS) < apply.1 > apply.man
X
Xapply.shar: apply.c getopt.h apply.1 Makefile
X	shar Makefile apply.c getopt.h apply.1 > apply.shar
X	chmod 400 apply.shar
END_OF_FILE
if test 415 -ne `wc -c <'Makefile'`; then
    echo shar: \"'Makefile'\" unpacked with wrong size!
fi
# end of 'Makefile'
fi
if test -f 'apply.c' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'apply.c'\"
else
echo shar: Extracting \"'apply.c'\" \(6256 characters\)
sed "s/^X//" >'apply.c' <<'END_OF_FILE'
X#include <sys/types.h> /* for wait and fork */
X#include <sys/wait.h> /* for wait */
X#include <stdio.h> /* for EOF, fprintf, puts */
X#include <malloc.h> /* for malloc and free */
X#include <strings.h> /* for strlen and strcpy */
X#include "getopt.h" /*XXX: should be <getopt.h> */
X/* XXX: What about system(), exit(), perror()? */
X/* XXX: Shouldn't do repeated malloc-free. Figure out the largest block */
X/*      size needed, allocate it once, and use it repeatedly. */
X/* XXX: Maybe option to quote arguments under flagsystem? */
X/* XXX: Or just allow spaces within arguments inside command? */
X/* XXX: The number of separate command parsers in here is ridiculous. */
X
Xstatic char applyusage[] = "Usage: apply [ -ac ] [ -n ] cmd arg ...\n";
Xstatic char applytoomany[] = "apply: warning: extra arguments\n";
Xstatic char applyforkfail[] = "apply: fatal: cannot fork";
Xstatic char applyexecfail[] = "apply: fatal: cannot exec";
Xstatic char applymalloc[] = "apply: fatal: cannot malloc\n";
X
Xmain(argc,argv)
Xint argc;
Xchar *argv[];
X{
X register char *s;
X register char *t;
X register char **a;
X register int i;
X register int j;
X register char *cmd;
X register int flagsystem = 0;
X register int narg = 1;
X register int nch = 0;
X register int nesc = 0;
X register char escape = '%';
X register int opt;
X
X while ((opt = getopt(argc,argv,"0123456789a:s")) != EOF)
X   switch(opt)
X    {
X     case '?': (void) fprintf(stderr,applyusage);
X	       (void) exit(1);
X     case 'a': escape = *optarg; break; /* can't be null */
X     case 's': flagsystem = 2; break;
X#define OSN(i,j) case i: narg = j; break;
X     OSN('0',0) OSN('1',1) OSN('2',2) OSN('3',3) OSN('4',4)
X     OSN('5',5) OSN('6',6) OSN('7',7) OSN('8',8) OSN('9',9)
X    }
X argv += optind, argc -= optind;
X
X if (!*argv)
X  {
X   (void) fprintf(stderr,applyusage);
X   (void) exit(1);
X  }
X
X /* Note that we accept %0, and do the same with it as with -0. */
X s = *argv;
X while (*s)
X   if (*(s++) != escape)
X     nch++;
X   else
X    {
X     if (!*s)
X      { flagsystem = (!flagsystem ? 1 : flagsystem); break; }
X     switch(*(s++))
X      {
X#define TSN(i,j) case i: narg=(nesc++?(narg>j?narg:j):j); break;
X       TSN('0',0) TSN('1',1) TSN('2',2) TSN('3',3) TSN('4',4)
X       TSN('5',5) TSN('6',6) TSN('7',7) TSN('8',8) TSN('9',9)
X       default: nch++; break;
X      }
X    }
X
X /* Now the number of real characters is nch. */
X /* The number of escape characters is nesc. */
X /* If flagsystem is 1, we should use sh. */
X /* And we should snarf narg args per cmd, plus waste one if !narg. */
X
X if (!nesc)
X  {
X   t = cmd = malloc((unsigned) (nch + 1));
X   s = *argv;
X   while (*s)
X     if (*s != escape)
X       *(t++) = *(s++);
X     else
X      {
X       if (!*++s)
X         break;
X       switch(*s)
X        {
X         case 'e': *(t++) = escape; s++; break;
X	 default: *(t++) = *(s++); break;
X        }
X      }
X   *t = '\0';
X  }
X else
X   cmd = *argv;
X   
X argc--; argv++;
X /* XXX: Should something be done to prevent zombies? */
X
X if (narg && (argc % narg))
X   (void) fprintf(stderr,applytoomany);
X while (argc >= narg + !narg)
X  {
X   if (!nesc)
X    {
X     if (flagsystem)
X      {
X       i = nch + narg + !narg;
X       for (j = 0;j < narg;j++)
X	 i += strlen(argv[j]);
X       t = s = malloc((unsigned) i);
X       if (!t)
X	{
X	 (void) fprintf(stderr,applymalloc);
X	 (void) exit(1);
X	}
X       (void) strcpy(t,cmd); t += strlen(cmd); *(t++) = ' ';
X       for (j = 0;j < narg;j++)
X	{
X         (void) strcpy(t,argv[j]); t += strlen(argv[j]); *(t++) = ' ';
X	}
X       *t = '\0';
X       /* slow? whaddya mean, slow? three passes is nothing! */
X       if (flagsystem == 2)
X         (void) puts(s);
X       else
X         (void) system(s);
X       (void) free(s);
X      }
X     else
X      {
X       s = argv[narg];
X       argv[narg] = (char *) 0;
X       argv[-1] = cmd; /* This always works. Trust me. */
X       /* We haven't done anything with stdio, so no need to flush. */
X#ifdef VFORK
X       switch(vfork())
X#else
X       switch(fork())
X#endif
X        {
X         case -1: perror(applyforkfail);
X		  (void) exit(1);
X         case 0: (void) execvp(cmd,argv - 1);
X	         perror(applyexecfail);
X	         (void) exit(0);
X         default: (void) wait((int *) 0);
X        }
X       argv[narg] = s;
X      }
X    }
X   else /* nesc > 0 */
X    {
X     i = nch + 1;
X     s = cmd;
X     while (*s)
X       if (*(s++) == escape)
X	{
X         if (!*s)
X           break;
X	 switch(*s)
X	  {
X	   case '0': s++; break;
X#define SSN(k,j) case k: i += strlen(argv[j]); s++; break;
X	   /* omit 0 */ SSN('1',0); SSN('2',1); SSN('3',2); SSN('4',3);
X	   SSN('5',4);  SSN('6',5); SSN('7',6); SSN('8',7); SSN('9',8);
X	   default: i += 1; s++; break;
X	  }
X	}
X
X     t = malloc((unsigned) i);
X     if (!t)
X      {
X       (void) fprintf(stderr,applymalloc);
X       (void) exit(1);
X      }
X     s = cmd;
X     while (*s)
X       if (*s != escape)
X	 *(t++) = *(s++);
X       else
X        {
X         if (!*++s)
X           break;
X	 switch(*s)
X	  {
X	   case '0': s++; break;
X#define ISN(i,j) case i:(void)strcpy(t,argv[j]);t+=strlen(argv[j]);s++;break;
X	   /* omit 0 */ ISN('1',0); ISN('2',1); ISN('3',2); ISN('4',3);
X	   ISN('5',4);  ISN('6',5); ISN('7',6); ISN('8',7); ISN('9',8);
X	   default: *(t++) = *(s++); break;
X	  }
X        }
X     *(t++) = '\0';
X     t -= i;
X     switch(flagsystem)
X      {
X       case 0: i = 1;
X	       for (s = t;*s;s++)
X		 i += (*s == ' ');
X	       a = (char **) malloc((unsigned) (sizeof(char *) * i));
X               if (!a)
X        	{
X        	 (void) fprintf(stderr,applymalloc);
X        	 (void) exit(1);
X        	}
X	       for (s = a[i = 0] = t;*s;s++)
X		 if (*s == ' ')
X		   a[a[i] == s ? i : ++i] = s + 1;
X	       a[a[i] == s ? i : ++i] = (char *) 0;
X#ifdef VFORK
X               switch(vfork())
X#else
X               switch(fork())
X#endif
X                {
X                 case -1: perror(applyforkfail);
X        		  (void) exit(1);
X                 case 0: (void) execvp(*a,a);
X        	         perror(applyexecfail);
X        	         (void) exit(0);
X                 default: (void) wait((int *) 0);
X                }
X	       (void) free((char *) a);
X	       break;
X       case 1: (void) system(t); break;
X       case 2: (void) puts(t); break;
X      }
X     (void) free(t);
X    }
X   argv += narg + !narg; argc -= narg + !narg;
X  }
X
X (void) exit(0);
X}
END_OF_FILE
if test 6256 -ne `wc -c <'apply.c'`; then
    echo shar: \"'apply.c'\" unpacked with wrong size!
fi
# end of 'apply.c'
fi
if test -f 'getopt.h' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'getopt.h'\"
else
echo shar: Extracting \"'getopt.h'\" \(61 characters\)
sed "s/^X//" >'getopt.h' <<'END_OF_FILE'
Xextern int optind;
Xextern char *optarg;
Xextern int getopt();
END_OF_FILE
if test 61 -ne `wc -c <'getopt.h'`; then
    echo shar: \"'getopt.h'\" unpacked with wrong size!
fi
# end of 'getopt.h'
fi
if test -f 'apply.1' -a "${1}" != "-c" ; then 
  echo shar: Will not clobber existing file \"'apply.1'\"
else
echo shar: Extracting \"'apply.1'\" \(2909 characters\)
sed "s/^X//" >'apply.1' <<'END_OF_FILE'
X.TH apply 1
X.SH NAME
Xapply \- apply a command to a set of arguments
X.SH SYNOPSIS
X.B apply
X[
X.B \-a
X.I escape-char
X] [
X.B \-s
X] [
X.I \-n
X]
X.I command
X[
X.I arg ...
X]
X.SH DESCRIPTION
X.B apply
Xapplies
X.I command
Xto each
X.I arg
Xin sequence.
X.PP
X.B apply
Xnormally runs
X.I command
Xwith one argument at a time.
XUnder
X.I \-2,
Xit will pass two arguments at once to
X.I command.
XSimilarly for all of
X.I \-1
Xthrough
X.I \-9.
XUnder
X.I \-0,
X.B apply
Xwill run
X.I command
Xwithout arguments, once for each
X.I arg.
X.PP
X.B apply
Xlooks for escape characters,
Xdefault %,
Xin
X.I command.
XIf
X.I %n
Xappears for any
X.I n
Xfrom 0 through 9,
X.B apply
Xwill place arguments
Xinside
X.I command
Xrather than after it:
Xthe first argument replacing
X.I %1,
Xetc.
XIt will also ignore any
X.I -n
Xoption,
Xinstead taking as many arguments per
X.I command
Xas the highest-numbered
X.I %n.
XIn this case
X.B apply
Xwill split
X.I command
Xon spaces,
Xalong with each
X.I arg;
Xthis is the only effect of
X.I %0,
Xwhich is deleted before
X.I command
Xis executed.
X.PP
X%% is passed through as a single %.
X%e is also passed through as a single %.
X% followed by any other character is passed through
Xas that character, though this behavior may not be true
Xin future versions.
XThe escape character may be changed with
X.B \-a.
X.PP
XNormally
X.I command
Xis executed directly
Xfrom 
X.B apply.
XIf
X.I command
Xends with a single %,
Xit is passed through
Xto
X.B sh(1)
Xvia
X.B system(3).
XThis lets
X.I command
Xuse output redirection and other shell features.
X.PP
XUnder
X.B \-s,
X.B apply
Xprints each
X.I command
Xrather than executing it.
XThis can be saved or piped through
X.B sh(1).
XIt is useful for debugging
Xuses of
X.B apply.
X.PP
X.B apply
Xwill ignore any extra arguments
Xand print a warning message.
X.SH EXAMPLES
XCompare the examples in
X.B xargs(1).
X.IP
X.B apply echo \(**
X.LP
XSimilar to
X.B ls(1).
X.IP
X\fBapply "mv $1/%1 $2/%1" `ls`\fR
X.LP
XMove all files from directory
X.B $1
Xto directory
X.B $2.
X.IP
X\fBapply \-s "mv $1/%1 $2/%1" \(** | sh \-v\fR
X.LP
XMove all files from directory
X.B $1
Xto directory
X.B $2,
Xprinting each command before it is executed.
X.IP
X.B apply \-2 diff $\(**
X.LP
XShow differences between successive pairs of files,
Xpassed to the shell as arguments.
X.IP
X\fBapply \-0 who 1 2 3 4 5\fR
X.LP
XRun
X.B who(1)
Xfive times.
X.IP
X.B apply \-2 %2 \(**
X.LP
XShow every other file.
X.IP
X.B apply \-2 %2 x \(**
X.LP
XShow the other every other file.
X.SH RESTRICTIONS
X.B apply
Xwill keep going blindly if
X.I command
Xisn't executable.
XOf course, this behavior is necessary
Xin commands like
X.IP
X.B
Xapply '%1 foo' rm touch 'chmod 755'
X.LP
XSometimes it is much faster to use
X.I sh,
Xas when
X.I command
Xis a shell builtin whose normal version is slow.
X.B apply
Xdoes not know to invoke
X.I sh
Xin this case.
X(This can happen when, for example,
X.I command
Xis dynamically linked
Xwhile
X.I sh
Xis not, on a system supporting dynamic linking.)
X.SH BUGS
XNone known.
X.SH "SEE ALSO"
X.B repeat(1), sh(1), xargs(1)
END_OF_FILE
if test 2909 -ne `wc -c <'apply.1'`; then
    echo shar: \"'apply.1'\" unpacked with wrong size!
fi
# end of 'apply.1'
fi
echo shar: End of shell archive.
exit 0