Commit message: When decompressing with -N or -NT, strip any path from header name.

This uses the path of the compressed file combined with the name
from the header as the name of the decompressed output file.  Any
path information in the header name is stripped.  This avoids a
possible vulnerability where absolute or descending paths are put
in the gzip header.
CVE: CVE-2015-1191
CWE: CWE-22
Vulnerability description: Multiple directory traversal vulnerabilities in pigz 2.3.1 allow remote attackers to write to arbitrary files via a (1) full pathname or (2) .. (dot dot) in an archive.

Project: pigz
File: pigz.c

local void process(char *path)
{
    int method = -1;                /* get_header() return value */
    size_t len;                     /* length of base name (minus suffix) */
    struct stat st;                 /* to get file type and mod time */
    /* all compressed suffixes for decoding search, in length order */
    static char *sufs[] = {".z", "-z", "_z", ".Z", ".gz", "-gz", ".zz", "-zz",
                           ".zip", ".ZIP", ".tgz", NULL};

    /* open input file with name in, descriptor ind -- set name and mtime */
    if (path == NULL) {
        strcpy(g.inf, "<stdin>");
        g.ind = 0;
        g.name = NULL;
        g.mtime = g.headis & 2 ?
                  (fstat(g.ind, &st) ? time(NULL) : st.st_mtime) : 0;
        len = 0;
    }
    else {
        /* set input file name (already set if recursed here) */
        if (path != g.inf) {
            strncpy(g.inf, path, sizeof(g.inf));
            if (g.inf[sizeof(g.inf) - 1])
                bail("name too long: ", path);
        }
        len = strlen(g.inf);

        /* try to stat input file -- if not there and decoding, look for that
           name with compressed suffixes */
        if (lstat(g.inf, &st)) {
            if (errno == ENOENT && (g.list || g.decode)) {
                char **try = sufs;
                do {
                    if (*try == NULL || len + strlen(*try) >= sizeof(g.inf))
                        break;
                    strcpy(g.inf + len, *try++);
                    errno = 0;
                } while (lstat(g.inf, &st) && errno == ENOENT);
            }
#ifdef EOVERFLOW
            if (errno == EOVERFLOW || errno == EFBIG)
                bail(g.inf,
                    " too large -- not compiled with large file support");
#endif
            if (errno) {
                g.inf[len] = 0;
                complain("%s does not exist -- skipping", g.inf);
                return;
            }
            len = strlen(g.inf);
        }

        /* only process regular files, but allow symbolic links if -f,
           recurse into directory if -r */
        if ((st.st_mode & S_IFMT) != S_IFREG &&
            (st.st_mode & S_IFMT) != S_IFLNK &&
            (st.st_mode & S_IFMT) != S_IFDIR) {
            complain("%s is a special file or device -- skipping", g.inf);
            return;
        }
        if ((st.st_mode & S_IFMT) == S_IFLNK && !g.force && !g.pipeout) {
            complain("%s is a symbolic link -- skipping", g.inf);
            return;
        }
        if ((st.st_mode & S_IFMT) == S_IFDIR && !g.recurse) {
            complain("%s is a directory -- skipping", g.inf);
            return;
        }

        /* recurse into directory (assumes Unix) */
        if ((st.st_mode & S_IFMT) == S_IFDIR) {
            char *roll, *item, *cut, *base, *bigger;
            size_t len, hold;
            DIR *here;
            struct dirent *next;

            /* accumulate list of entries (need to do this, since readdir()
               behavior not defined if directory modified between calls) */
            here = opendir(g.inf);
            if (here == NULL)
                return;
            hold = 512;
            roll = MALLOC(hold);
            if (roll == NULL)
                bail("not enough memory", "");
            *roll = 0;
            item = roll;
            while ((next = readdir(here)) != NULL) {
                if (next->d_name[0] == 0 ||
                    (next->d_name[0] == '.' && (next->d_name[1] == 0 ||
                     (next->d_name[1] == '.' && next->d_name[2] == 0))))
                    continue;
                len = strlen(next->d_name) + 1;
                if (item + len + 1 > roll + hold) {
                    do {                    /* make roll bigger */
                        hold <<= 1;
                    } while (item + len + 1 > roll + hold);
                    bigger = REALLOC(roll, hold);
                    if (bigger == NULL) {
                        FREE(roll);
                        bail("not enough memory", "");
                    }
                    item = bigger + (item - roll);
                    roll = bigger;
                }
                strcpy(item, next->d_name);
                item += len;
                *item = 0;
            }
            closedir(here);

            /* run process() for each entry in the directory */
            cut = base = g.inf + strlen(g.inf);
            if (base > g.inf && base[-1] != (unsigned char)'/') {
                if ((size_t)(base - g.inf) >= sizeof(g.inf))
                    bail("path too long", g.inf);
                *base++ = '/';
            }
            item = roll;
            while (*item) {
                strncpy(base, item, sizeof(g.inf) - (base - g.inf));
                if (g.inf[sizeof(g.inf) - 1]) {
                    strcpy(g.inf + (sizeof(g.inf) - 4), "...");
                    bail("path too long: ", g.inf);
                }
                process(g.inf);
                item += strlen(item) + 1;
            }
            *cut = 0;

            /* release list of entries */
            FREE(roll);
            return;
        }

        /* don't compress .gz (or provided suffix) files, unless -f */
        if (!(g.force || g.list || g.decode) && len >= strlen(g.sufx) &&
                strcmp(g.inf + len - strlen(g.sufx), g.sufx) == 0) {
            complain("%s ends with %s -- skipping", g.inf, g.sufx);
            return;
        }

        /* create output file only if input file has compressed suffix */
        if (g.decode == 1 && !g.pipeout && !g.list) {
            int suf = compressed_suffix(g.inf);
            if (suf == 0) {
                complain("%s does not have compressed suffix -- skipping",
                         g.inf);
                return;
            }
            len -= suf;
        }

        /* open input file */
        g.ind = open(g.inf, O_RDONLY, 0);
        if (g.ind < 0)
            bail("read error on ", g.inf);

        /* prepare gzip header information for compression */
        g.name = g.headis & 1 ? justname(g.inf) : NULL;
        g.mtime = g.headis & 2 ? st.st_mtime : 0;
    }
    SET_BINARY_MODE(g.ind);

    /* if decoding or testing, try to read gzip header */
    g.hname = NULL;
    if (g.decode) {
        in_init();
        method = get_header(1);
        if (method != 8 && method != 257 &&
                /* gzip -cdf acts like cat on uncompressed input */
                !(method == -2 && g.force && g.pipeout && g.decode != 2 &&
                  !g.list)) {
            RELEASE(g.hname);
            if (g.ind != 0)
                close(g.ind);
            if (method != -1)
                complain(method < 0 ? "%s is not compressed -- skipping" :
                         "%s has unknown compression method -- skipping",
                         g.inf);
            return;
        }

        /* if requested, test input file (possibly a special list) */
        if (g.decode == 2) {
            if (method == 8)
                infchk();
            else {
                unlzw();
                if (g.list) {
                    g.in_tot -= 3;
                    show_info(method, 0, g.out_tot, 0);
                }
            }
            RELEASE(g.hname);
            if (g.ind != 0)
                close(g.ind);
            return;
        }
    }

    /* if requested, just list information about input file */
    if (g.list) {
        list_info();
        RELEASE(g.hname);
        if (g.ind != 0)
            close(g.ind);
        return;
    }

    /* create output file out, descriptor outd */
    if (path == NULL || g.pipeout) {
        /* write to stdout */
        g.outf = MALLOC(strlen("<stdout>") + 1);
        if (g.outf == NULL)
            bail("not enough memory", "");
        strcpy(g.outf, "<stdout>");
        g.outd = 1;
        if (!g.decode && !g.force && isatty(g.outd))
            bail("trying to write compressed data to a terminal",
                  " (use -f to force)");
     }
     else {
        char *to = g.inf, *sufx = "";
        size_t pre = 0;

        /* select parts of the output file name */
        if (g.decode) {
            /* for -dN or -dNT, use the path from the input file and the name
               from the header, stripping any path in the header name */
            if ((g.headis & 1) != 0 && g.hname != NULL) {
                pre = justname(g.inf) - g.inf;
                to = justname(g.hname);
                len = strlen(to);
            }
            /* for -d or -dNn, replace abbreviated suffixes */
            else if (strcmp(to + len, ".tgz") == 0)
                sufx = ".tar";
         }
        else
            /* add appropriate suffix when compressing */
            sufx = g.sufx;
 
         /* create output file and open to write */
        g.outf = MALLOC(pre + len + strlen(sufx) + 1);
         if (g.outf == NULL)
             bail("not enough memory", "");
        memcpy(g.outf, g.inf, pre);
        memcpy(g.outf + pre, to, len);
        strcpy(g.outf + pre + len, sufx);
         g.outd = open(g.outf, O_CREAT | O_TRUNC | O_WRONLY |
                              (g.force ? 0 : O_EXCL), 0600);
 
         /* if exists and not -f, give user a chance to overwrite */
         if (g.outd < 0 && errno == EEXIST && isatty(0) && g.verbosity) {
            int ch, reply;

            fprintf(stderr, "%s exists -- overwrite (y/n)? ", g.outf);
            fflush(stderr);
            reply = -1;
            do {
                ch = getchar();
                if (reply < 0 && ch != ' ' && ch != '\t')
                    reply = ch == 'y' || ch == 'Y' ? 1 : 0;
            } while (ch != EOF && ch != '\n' && ch != '\r');
            if (reply == 1)
                g.outd = open(g.outf, O_CREAT | O_TRUNC | O_WRONLY,
                              0600);
        }

        /* if exists and no overwrite, report and go on to next */
        if (g.outd < 0 && errno == EEXIST) {
            complain("%s exists -- skipping", g.outf);
            RELEASE(g.outf);
            RELEASE(g.hname);
            if (g.ind != 0)
                close(g.ind);
            return;
        }

        /* if some other error, give up */
        if (g.outd < 0)
            bail("write error on ", g.outf);
    }
    SET_BINARY_MODE(g.outd);
    RELEASE(g.hname);

    /* process ind to outd */
    if (g.verbosity > 1)
        fprintf(stderr, "%s to %s ", g.inf, g.outf);
    if (g.decode) {
        if (method == 8)
            infchk();
        else if (method == 257)
            unlzw();
        else
            cat();
    }
#ifndef NOTHREAD
    else if (g.procs > 1)
        parallel_compress();
#endif
    else
        single_compress(0);
    if (g.verbosity > 1) {
        putc('\n', stderr);
        fflush(stderr);
    }

    /* finish up, copy attributes, set times, delete original */
    if (g.ind != 0)
        close(g.ind);
    if (g.outd != 1) {
        if (close(g.outd))
            bail("write error on ", g.outf);
        g.outd = -1;            /* now prevent deletion on interrupt */
        if (g.ind != 0) {
            copymeta(g.inf, g.outf);
            if (!g.keep)
                unlink(g.inf);
        }
        if (g.decode && (g.headis & 2) != 0 && g.stamp)
            touch(g.outf, g.stamp);
    }
    RELEASE(g.outf);
}