From a711b8681aced24f90e90e0560cfb00d7d71086f Mon Sep 17 00:00:00 2001
From: jkaluza <jkaluza@ec1272ba-9ed1-42ef-8245-99669996828e>
Date: Wed, 9 Mar 2011 09:41:46 +0000
Subject: [PATCH logrotate] Backport svn rev 317, 358-360 (bnc#677335)

- don't accept service owned log directories anymore
- don't run external programs with uid != euid
  - switch euid back to root before running external scripts
  - run compressors and shred as unprivileged user
- add O_NOFOLLOW when opening files as safeguard against symlink tricks
- add "su" config option
---
 config.c    |   44 ++++++++++++++++++
 logrotate.8 |    5 ++
 logrotate.c |  144 +++++++++++++++++++++++++++++++++++++++++++++++++++++-----
 logrotate.h |    3 +
 4 files changed, 183 insertions(+), 13 deletions(-)

diff --git a/config.c b/config.c
index d59fb62..3b8b15f 100644
--- a/config.c
+++ b/config.c
@@ -443,6 +443,7 @@ int readAllConfigPaths(const char **paths)
 	/* flags */ LOG_FLAG_IFEMPTY,
 	/* shred_cycles */ 0,
 	/* createMode/Uid/Gid */ NO_MODE, NO_UID, NO_GID,
+	/* suUid/suGid */ 0, 0,
 	/* compress_options_list/count */ NULL, 0
     };
 
@@ -722,6 +723,49 @@ static int readConfigFile(const char *configFile, struct logInfo *defConfig)
 		newlog->flags &= ~LOG_FLAG_MAILFIRST;
 
 		*endtag = oldchar, start = endtag;
+	    } else if (!strcmp(start, "su")) {
+		*endtag = oldchar, start = endtag;
+
+		endtag = start;
+		while (*endtag != '\n')
+		    endtag++;
+		while (isspace(*endtag))
+		    endtag--;
+		endtag++;
+		oldchar = *endtag, *endtag = '\0';
+
+		rc = sscanf(start, "%200s %200s%c", createOwner,
+			createGroup, &foo);
+		if (rc == 3) {
+		    message(MESS_ERROR, "%s:%d extra arguments for "
+			    "su\n", configFile, lineNum);
+		    return 1;
+		}
+
+		if (rc > 0) {
+		    pw = getpwnam(createOwner);
+		    if (!pw) {
+			message(MESS_ERROR, "%s:%d unknown user '%s'\n",
+				configFile, lineNum, createOwner);
+			return 1;
+		    }
+		    newlog->suUid = pw->pw_uid;
+		    endpwent();
+		}
+		if (rc > 1) {
+		    group = getgrnam(createGroup);
+		    if (!group) {
+			message(MESS_ERROR, "%s:%d unknown group '%s'\n",
+				configFile, lineNum, createGroup);
+			return 1;
+		    }
+		    newlog->suGid = group->gr_gid;
+		    endgrent();
+		}
+
+		newlog->flags |= LOG_FLAG_SU;
+
+		*endtag = oldchar, start = endtag;
 	    } else if (!strcmp(start, "create")) {
 		*endtag = oldchar, start = endtag;
 
diff --git a/logrotate.8 b/logrotate.8
index 19abebd..c9d9ed7 100644
--- a/logrotate.8
+++ b/logrotate.8
@@ -443,6 +443,11 @@ be created with a .9, skipping 0-8.  Files will still be rotated the
 number of times specified with the \fBcount\fR directive.
 
 .TP
+\fBsu \fIuser\fR \fIgroup\fR
+Rotate log files with the specified user and group instead of using
+the default (usually root).
+
+.TP
 \fBtabooext\fR [+] \fIlist\fR
 The current taboo extension list is changed (see the \fBinclude\fR directive
 for information on the taboo extensions). If a + precedes the list of
diff --git a/logrotate.c b/logrotate.c
index 0ce368c..e325d3f 100644
--- a/logrotate.c
+++ b/logrotate.c
@@ -70,6 +70,8 @@ const char * compress_cmd_list[][2] = {
 };
 
 time_t nowSecs = 0;
+static uid_t save_euid;
+static gid_t save_egid;
 
 static int shred_file(int fd, char *filename, struct logInfo *log);
 
@@ -82,6 +84,48 @@ static int globerr(const char *pathname, int theerr)
     return 1;
 }
 
+int switch_user(uid_t user, gid_t group) {
+	save_egid = getegid();
+	save_euid = geteuid();
+	if (save_euid == user && save_egid == group)
+		return 0;
+	message(MESS_DEBUG, "switching euid to %d and egid to %d\n",
+		user, group);
+	if (setegid(group) || seteuid(user)) {
+		message(MESS_ERROR, "error switching euid to %d and egid to %d: %s\n",
+			user, group, strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
+int switch_user_back() {
+	return switch_user(save_euid, save_egid);
+}
+
+int switch_user_permanently(const struct logInfo *log) {
+	gid_t group = getegid();
+	uid_t user = geteuid();
+	if (!(log->flags & LOG_FLAG_SU)) {
+		return 0;
+	}
+	if (getuid() == user && getgid() == group)
+		return 0;
+	// switch to full root first
+	if (setgid(getgid()) || setuid(getuid())) {
+		message(MESS_ERROR, "error getting rid of euid != uid\n");
+		return 1;
+	}
+	message(MESS_DEBUG, "switching uid to %d and gid to %d\n",
+		user, group);
+	if (setgid(group) || setuid(user)) {
+		message(MESS_ERROR, "error switching euid to %d and egid to %d: %s\n",
+			user, group, strerror(errno));
+		return 1;
+	}
+	return 0;
+}
+
 static void unescape(char *arg)
 {
 	char *p = arg;
@@ -209,7 +253,7 @@ static struct logState *findState(const char *fn)
 	return p;
 }
 
-static int runScript(char *logfn, char *script)
+static int runScript(struct logInfo *log, char *logfn, char *script)
 {
     int rc;
 
@@ -220,6 +264,12 @@ static int runScript(char *logfn, char *script)
     }
 
 	if (!fork()) {
+		if (log->flags & LOG_FLAG_SU) {
+			if (switch_user_back() != 0) {
+				exit(1);
+			}
+		}
+
 		execl("/bin/sh", "sh", "-c", script, "logrotate_script", logfn, NULL);
 		exit(1);
 	}
@@ -298,6 +348,10 @@ static int shred_file(int fd, char *filename, struct logInfo *log)
 		dup2(fd, 1);
 		close(fd);
 
+		if (switch_user_permanently(log) != 0) {
+			exit(1);
+		}
+
 		execvp(fullCommand[0], (void *) fullCommand);
 		exit(1);
 		}
@@ -319,7 +373,7 @@ static int removeLogFile(char *name, struct logInfo *log)
 	int fd;
     message(MESS_DEBUG, "removing old log %s\n", name);
 
-	if ((fd = open(name, O_RDWR)) < 0) {
+	if ((fd = open(name, O_RDWR | O_NOFOLLOW)) < 0) {
 		message(MESS_ERROR, "error opening %s: %s\n",
 			name, strerror(errno));
 		return 1;
@@ -359,7 +413,7 @@ static int compressLogFile(char *name, struct logInfo *log, struct stat *sb)
     compressedName = alloca(strlen(name) + strlen(log->compress_ext) + 2);
     sprintf(compressedName, "%s%s", name, log->compress_ext);
 
-    if ((inFile = open(name, O_RDWR)) < 0) {
+    if ((inFile = open(name, O_RDWR | O_NOFOLLOW)) < 0) {
 	message(MESS_ERROR, "unable to open %s for compression\n", name);
 	return 1;
     }
@@ -377,6 +431,10 @@ static int compressLogFile(char *name, struct logInfo *log, struct stat *sb)
 	dup2(outFile, 1);
 	close(outFile);
 
+	if (switch_user_permanently(log) != 0) {
+		exit(1);
+	}
+
 	execvp(fullCommand[0], (void *) fullCommand);
 	exit(1);
     }
@@ -396,7 +454,7 @@ static int compressLogFile(char *name, struct logInfo *log, struct stat *sb)
     return 0;
 }
 
-static int mailLog(char *logFile, char *mailCommand,
+static int mailLog(const struct logInfo *log, char *logFile, char *mailCommand,
 		   char *uncompressCommand, char *address, char *subject)
 {
     int mailInput;
@@ -406,7 +464,7 @@ static int mailLog(char *logFile, char *mailCommand,
     char *mailArgv[] = { mailCommand, "-s", subject, address, NULL };
     int rc = 0;
 
-    if ((mailInput = open(logFile, O_RDONLY)) < 0) {
+    if ((mailInput = open(logFile, O_RDONLY | O_NOFOLLOW)) < 0) {
 	message(MESS_ERROR, "failed to open %s for mailing: %s\n", logFile,
 		strerror(errno));
 	return 1;
@@ -422,6 +480,10 @@ static int mailLog(char *logFile, char *mailCommand,
 	    close(uncompressPipe[0]);
 	    close(uncompressPipe[1]);
 
+	    if (switch_user_permanently(log) != 0) {
+		    exit(1);
+	    }
+
 	    execlp(uncompressCommand, uncompressCommand, NULL);
 	    exit(1);
 	}
@@ -436,6 +498,13 @@ static int mailLog(char *logFile, char *mailCommand,
 	close(mailInput);
 	close(1);
 
+	// mail command runs as root
+	if (log->flags & LOG_FLAG_SU) {
+		if (switch_user_back() != 0) {
+			exit(1);
+		}
+	}
+
 	execvp(mailArgv[0], mailArgv);
 	exit(1);
     }
@@ -471,12 +540,12 @@ static int mailLogWrapper(char *mailFilename, char *mailCommand,
     if ((log->flags & LOG_FLAG_COMPRESS) &&
 	!((log->flags & LOG_FLAG_DELAYCOMPRESS) &&
 	  (log->flags & LOG_FLAG_MAILFIRST))) {
-	if (mailLog(mailFilename, mailCommand,
+	if (mailLog(log, mailFilename, mailCommand,
 		    log->uncompress_prog, log->logAddress,
 		    log->files[logNum]))
 	    return 1;
     } else {
-	if (mailLog(mailFilename, mailCommand, NULL,
+	if (mailLog(log, mailFilename, mailCommand, NULL,
 		    log->logAddress, mailFilename))
 	    return 1;
     }
@@ -493,7 +562,7 @@ static int copyTruncate(char *currLog, char *saveLog, struct stat *sb,
     message(MESS_DEBUG, "copying %s to %s\n", currLog, saveLog);
 
     if (!debug) {
-	if ((fdcurr = open(currLog, O_RDWR)) < 0) {
+	if ((fdcurr = open(currLog, O_RDWR | O_NOFOLLOW)) < 0) {
 	    message(MESS_ERROR, "error opening %s: %s\n", currLog,
 		    strerror(errno));
 	    return 1;
@@ -599,6 +668,36 @@ int findNeedRotating(struct logInfo *log, int logNum)
 
     message(MESS_DEBUG, "considering log %s\n", log->files[logNum]);
 
+	/* Check if parent directory of this log has safe permissions */
+    if ((log->flags & LOG_FLAG_SU) == 0 && getuid() == 0) {
+	char *ld = ourDirName(log->files[logNum]);
+	if (stat(ld, &sb)) {
+	    /* If parent directory doesn't exist, it's not real error
+	       and rotation is not needed */
+	    if (errno != ENOENT) {
+		message(MESS_ERROR, "stat of %s failed: %s\n", ld,
+			strerror(errno));
+		free(ld);
+		return 1;
+	    }
+	    free(ld);
+	    return 0;
+	}
+	/* Don't rotate in directories writable by others or group which is not "root"  */
+	if (sb.st_uid != 0 || (sb.st_gid != 0 && sb.st_mode & S_IWGRP) || sb.st_mode & S_IWOTH) {
+	    message(MESS_ERROR, "\"%s\" has insecure permissions."
+		    " It must be owned and be writable by root only to avoid security problems."
+		    " Set the \"su\" directive in the config file to tell logrotate which user/group"
+		    " should be used for rotation.\n"
+		    , ld);
+#if 0 // accept nevertheless for now
+	    free(ld);
+	    return 0;
+#endif
+	}
+	free(ld);
+    }
+
     if (stat(log->files[logNum], &sb)) {
 	if ((log->flags & LOG_FLAG_MISSINGOK) && (errno == ENOENT)) {
 	    message(MESS_DEBUG, "  log %s does not exist -- skipping\n",
@@ -1088,7 +1187,7 @@ int rotateSingleLog(struct logInfo *log, int logNum, struct logState *state,
 			security_context_t oldContext = NULL;
 			int fdcurr = -1;
 
-			if ((fdcurr = open(log->files[logNum], O_RDWR)) < 0) {
+			if ((fdcurr = open(log->files[logNum], O_RDWR | O_NOFOLLOW)) < 0) {
 				message(MESS_ERROR, "error opening %s: %s\n",
 						log->files[logNum],
 					strerror(errno));
@@ -1316,6 +1415,12 @@ int rotateLogSet(struct logInfo *log, int force)
 	message(MESS_DEBUG, "old logs are removed\n");
     }
 
+    if (log->flags & LOG_FLAG_SU) {
+	if (switch_user(log->suUid, log->suGid) != 0) {
+	    return 1;
+	}
+    }
+
     for (i = 0; i < log->numFiles; i++) {
 	logHasErrors[i] = findNeedRotating(log, i);
 	hasErrors |= logHasErrors[i];
@@ -1331,10 +1436,17 @@ int rotateLogSet(struct logInfo *log, int force)
 		    "since no logs will be rotated\n");
 	} else {
 	    message(MESS_DEBUG, "running first action script\n");
-	    if (runScript(log->pattern, log->first)) {
+	    if (runScript(log, log->pattern, log->first)) {
 		message(MESS_ERROR, "error running first action script "
 			"for %s\n", log->pattern);
 		hasErrors = 1;
+
+		if (log->flags & LOG_FLAG_SU) {
+		    if (switch_user_back() != 0) {
+			return 1;
+		    }
+		}
+
 		/* finish early, firstaction failed, affects all logs in set */
 		return hasErrors;
 	    }
@@ -1369,7 +1481,7 @@ int rotateLogSet(struct logInfo *log, int force)
 			"since no logs will be rotated\n");
 	    } else {
 		message(MESS_DEBUG, "running prerotate script\n");
-		if (runScript(log->pattern, log->pre)) {
+		if (runScript(log, log->pattern, log->pre)) {
 		    if (log->flags & LOG_FLAG_SHAREDSCRIPTS)
 			message(MESS_ERROR,
 				"error running shared prerotate script "
@@ -1404,7 +1516,7 @@ int rotateLogSet(struct logInfo *log, int force)
 			"since no logs were rotated\n");
 	    } else {
 		message(MESS_DEBUG, "running postrotate script\n");
-		if (runScript(log->pattern, log->post)) {
+		if (runScript(log, log->pattern, log->post)) {
 		    if (log->flags & LOG_FLAG_SHAREDSCRIPTS)
 			message(MESS_ERROR,
 				"error running shared postrotate script "
@@ -1450,7 +1562,7 @@ int rotateLogSet(struct logInfo *log, int force)
 		    "since no logs will be rotated\n");
 	} else {
 	    message(MESS_DEBUG, "running last action script\n");
-	    if (runScript(log->pattern, log->last)) {
+	    if (runScript(log, log->pattern, log->last)) {
 		message(MESS_ERROR, "error running last action script "
 			"for %s\n", log->pattern);
 		hasErrors = 1;
@@ -1458,6 +1570,12 @@ int rotateLogSet(struct logInfo *log, int force)
 	}
     }
 
+    if (log->flags & LOG_FLAG_SU) {
+	if (switch_user_back() != 0) {
+	    return 1;
+	}
+    }
+
     return hasErrors;
 }
 
diff --git a/logrotate.h b/logrotate.h
index 11e434c..578780f 100644
--- a/logrotate.h
+++ b/logrotate.h
@@ -18,6 +18,7 @@
 #define LOG_FLAG_COPY		(1 << 8)
 #define LOG_FLAG_DATEEXT	(1 << 9)
 #define LOG_FLAG_SHRED		(1 << 10)
+#define LOG_FLAG_SU		(1 << 11)
 
 #define NO_MODE ((mode_t) -1)
 #define NO_UID  ((uid_t) -1)
@@ -51,6 +52,8 @@ struct logInfo {
     mode_t createMode;		/* if any/all of these are -1, we use the */
     uid_t createUid;		/* attributes from the log file just rotated */
     gid_t createGid;
+    uid_t suUid;			/* switch user to this uid and group to this gid */
+    gid_t suGid;
     /* these are at the end so they end up nil */
     const char **compress_options_list;
     int compress_options_count;
-- 
1.7.7

