CVE-2017-7501: Improper Link Resolution Before File Access ('Link Following') (CWE-59)
value: It was found that versions of rpm before 4.13.0.2 use temporary files with predictable names when installing an RPM. An attacker with ability to write in a directory where files will be installed could create symbolic links to an arbitrary location and modify content, and possibly permissions to arbitrary files, which could be used for denial of service or possibly privilege escalation.
CWE description: The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource.
CVSS v3 base score: 7.8
Published: 2017-11-22T22:29Z
Repository: https://github.com/rpm-software-management/rpm
Language: C
Files changed: lib/fsm.c
Commit date: 2017-09-19 14:46:36 +0300
Commit message: Don't follow symlinks on file creation (CVE-2017-7501)

Open newly created files with O_EXCL to prevent symlink tricks.
When reopening hardlinks for writing the actual content, use append
mode instead. This is compatible with the write-only permissions but
is not destructive in case we got redirected to somebody elses file,
verify the target before actually writing anything.

As these are files with the temporary suffix, errors mean a local
user with sufficient privileges to break the installation of the package
anyway is trying to goof us on purpose, don't bother trying to mend it
(we couldn't fix the hardlink case anyhow) but just bail out.

Based on a patch by Florian Festi.

Diff: diff --git a/lib/fsm.c b/lib/fsm.c
index 553774b30..e0e9d03a1 100644
--- a/lib/fsm.c
+++ b/lib/fsm.c
@@ -204,24 +204,41 @@ static int fsmSetFCaps(const char *path, const char *captxt)
     } 
 #endif
     return rc;
 }
 
+/* Check dest is the same, empty and regular file with writeonly permissions */
+static int linkSane(FD_t wfd, const char *dest)
+{
+    struct stat sb, lsb;
+
+    return (fstat(Fileno(wfd), &sb) == 0 && sb.st_size == 0 &&
+	    (sb.st_mode & ~S_IFMT) == S_IWUSR &&
+	    lstat(dest, &lsb) == 0 && S_ISREG(lsb.st_mode) &&
+	    sb.st_dev == lsb.st_dev && sb.st_ino == lsb.st_ino);
+}
+
 /** \ingroup payload
  * Create file from payload stream.
  * @return		0 on success
  */
-static int expandRegular(rpmfi fi, const char *dest, rpmpsm psm, int nodigest, int nocontent)
+static int expandRegular(rpmfi fi, const char *dest, rpmpsm psm, int exclusive, int nodigest, int nocontent)
 {
     FD_t wfd = NULL;
     int rc = 0;
 
     /* Create the file with 0200 permissions (write by owner). */
     {
 	mode_t old_umask = umask(0577);
-	wfd = Fopen(dest, "w.ufdio");
+	wfd = Fopen(dest, exclusive ? "wx.ufdio" : "a.ufdio");
 	umask(old_umask);
+
+	/* If reopening, make sure the file is what we expect */
+	if (!exclusive && wfd != NULL && !linkSane(wfd, dest)) {
+	    rc = RPMERR_OPEN_FAILED;
+	    goto exit;
+	}
     }
     if (Ferror(wfd)) {
 	rc = RPMERR_OPEN_FAILED;
 	goto exit;
     }
@@ -246,11 +263,11 @@ static int fsmMkfile(rpmfi fi, const char *dest, rpmfiles files,
 
     if (numHardlinks > 1) {
 	/* Create first hardlinked file empty */
 	if (*firsthardlink < 0) {
 	    *firsthardlink = rpmfiFX(fi);
-	    rc = expandRegular(fi, dest, psm, nodigest, 1);
+	    rc = expandRegular(fi, dest, psm, 1, nodigest, 1);
 	} else {
 	    /* Create hard links for others */
 	    char *fn = rpmfilesFN(files, *firsthardlink);
 	    rc = link(fn, dest);
 	    if (rc < 0) {
@@ -261,14 +278,14 @@ static int fsmMkfile(rpmfi fi, const char *dest, rpmfiles files,
     }
     /* Write normal files or fill the last hardlinked (already
        existing) file with content */
     if (numHardlinks<=1) {
 	if (!rc)
-	    rc = expandRegular(fi, dest, psm, nodigest, 0);
+	    rc = expandRegular(fi, dest, psm, 1, nodigest, 0);
     } else if (rpmfiArchiveHasContent(fi)) {
 	if (!rc)
-	    rc = expandRegular(fi, dest, psm, nodigest, 0);
+	    rc = expandRegular(fi, dest, psm, 0, nodigest, 0);
 	*firsthardlink = -1;
     } else {
 	*setmeta = 0;
     }
 
@@ -937,11 +954,11 @@ int rpmPackageFilesInstall(rpmts ts, rpmte te, rpmfiles files,
 	    }
         } else if (firsthardlink >= 0 && rpmfiArchiveHasContent(fi)) {
 	    /* we skip the hard linked file containing the content */
 	    /* write the content to the first used instead */
 	    char *fn = rpmfilesFN(files, firsthardlink);
-	    rc = expandRegular(fi, fn, psm, nodigest, 0);
+	    rc = expandRegular(fi, fn, psm, 0, nodigest, 0);
 	    firsthardlink = -1;
 	    free(fn);
 	}
 
         if (rc) {

Vulnerable code: static int expandRegular(rpmfi fi, const char *dest, rpmpsm psm, int nodigest, int nocontent)
	wfd = Fopen(dest, "w.ufdio");
	    rc = expandRegular(fi, dest, psm, nodigest, 1);
	    rc = expandRegular(fi, dest, psm, nodigest, 0);
	    rc = expandRegular(fi, dest, psm, nodigest, 0);
	    rc = expandRegular(fi, fn, psm, nodigest, 0);
Fixed code: /* Check dest is the same, empty and regular file with writeonly permissions */
static int linkSane(FD_t wfd, const char *dest)
{
    struct stat sb, lsb;

    return (fstat(Fileno(wfd), &sb) == 0 && sb.st_size == 0 &&
	    (sb.st_mode & ~S_IFMT) == S_IWUSR &&
	    lstat(dest, &lsb) == 0 && S_ISREG(lsb.st_mode) &&
	    sb.st_dev == lsb.st_dev && sb.st_ino == lsb.st_ino);
}

static int expandRegular(rpmfi fi, const char *dest, rpmpsm psm, int exclusive, int nodigest, int nocontent)
	wfd = Fopen(dest, exclusive ? "wx.ufdio" : "a.ufdio");

	/* If reopening, make sure the file is what we expect */
	if (!exclusive && wfd != NULL && !linkSane(wfd, dest)) {
	    rc = RPMERR_OPEN_FAILED;
	    goto exit;
	}
	    rc = expandRegular(fi, dest, psm, 1, nodigest, 1);
	    rc = expandRegular(fi, dest, psm, 1, nodigest, 0);
	    rc = expandRegular(fi, dest, psm, 0, nodigest, 0);
	    rc = expandRegular(fi, fn, psm, 0, nodigest, 0);