<sect1 id="changes">
<title>Changes</title>
+ <itemizedlist>
+ <title>From version 3.0.5 to 3.1.0</title>
+ <listitem>
+ <para>Pass fcrondyn client credentials through the socket when possible so as the user doesn't need to type his password when using fcrondyn.</para>
+ </listitem>
+ <listitem>
+ <para>Code clean-up: Implemented generic unordred list for lavgq / exeq.</para>
+ </listitem>
+ </itemizedlist>
+
<itemizedlist>
<title>From version 3.0.4 to 3.0.5</title>
+ <listitem>
+ <para>Security issue: fixed security issue that allowed malicious user to read any file readable by group 'fcron' (in particular fcron's config files and the fcrontabs of non-root users). On systems without seteuid() (i.e. quite exotic systems) then the bug is harder to exploit but any file could be read.</para>
+ </listitem>
+ <listitem>
+ <para>Improved general security of fcrontab and fcrondyn by dropping more privileges.</para>
+ </listitem>
<listitem>
<para>Work on portability (aix, hpux, irix, tru64 unix, solaris). Thanks Peter O'Gorman for his patch!</para>
</listitem>
sgml-parent-document:("fcron-doc.sgml" "book" "chapter" "sect1" "")
End:
-->
--
while ( (c = getc(stdin)) != EOF )
putc(c, tmp_file);
- debug("Copied stdin to %s, about to parse file %s...", tmp_str, tmp_str);
+ /* // */
++ debug("Copied stdin to %s, about to parse file %s...", tmp_str, tmp_str);
- /* the following closes tmp_fd as well because it was fdopen()ed: */
- fclose(tmp_file);
+ /* don't closes tmp_fd as it will be used for make_file(): */
+ if ( fflush(tmp_file) != 0 )
+ die_e("Could not fflush(%s)", tmp_file);
- if ( make_file(tmp_str) == ERR )
+ if ( make_file(tmp_str, tmp_fd) == ERR )
goto exiterr;
else
goto exit;
if (strrchr(argv[0],'/')==NULL) prog_name = argv[0];
else prog_name = strrchr(argv[0],'/')+1;
- uid = getuid();
+ useruid = getuid();
+ usergid = getgid();
+
+ #ifdef USE_SETE_ID
+ /* drop any suid privilege (that we use to write files) but keep sgid
+ * one for now: we need it for read_conf() and is_allowed() */
- seteuid_safe(useruid);
++ seteuid_safe(useruid);
+ #endif
errno = 0;
if ( ! (pass = getpwnam(USERNAME)) )
parseopt(argc, argv);
#ifdef USE_SETE_ID
- seteuid_safe(useruid);
- setegid_safe(usergid);
+ /* drop any privilege we may have: we will only get them back
+ * temporarily every time we need it. */
++ seteuid_safe(useruid);
++ setegid_safe(usergid);
+ #endif
#ifdef HAVE_LIBPAM
/* Open PAM session for the user and obtain any security
}
+void
+init_default_line(cl_t *cl, cf_t *cf)
+/* clear all context/options from cl */
+{
+ bzero(cl, sizeof(cl_t));
+ Set(cl->cl_runas, runas);
+ Set(cl->cl_mailto, runas);
+ free_safe(cl->cl_tz);
+ set_default_opt(cl->cl_option);
+ cl->cl_file = cf;
+}
+
+
int
- read_file(char *filename)
+ read_file(char *filename, int fd)
/* read file "name" and append cf_t list */
{
cf_t *cf = NULL;
return ERR;
}
+ /* Rewind, just in case */
+ rewind(file);
+
Alloc(cf, cf_t);
+ cf->cf_env_list = env_list_init();
cf->cf_user = strdup2(user);
- default_line.cl_file = cf;
- default_line.cl_runas = strdup2(runas);
- default_line.cl_mailto = strdup2(runas);
- default_line.cl_tz = NULL;
- set_default_opt(default_line.cl_option);
+ init_default_line(&default_line, cf);
if ( debug_opt )
fprintf(stderr, "FILE %s\n", file_name);
cf->cf_next = file_base;
file_base = cf;
- fclose(file);
+ /* don't close as underlying fd may still be used by calling function */
+ if (fflush(file) != 0)
+ error_e("could not fflush() file_name");
- free(default_line.cl_runas);
- free(default_line.cl_mailto);
- free(default_line.cl_tz);
+ free_safe(default_line.cl_runas);
+ free_safe(default_line.cl_mailto);
+ free_safe(default_line.cl_tz);
if ( ! need_correction )
return OK;
#include "fcron.h"
#include "job.h"
+#include "temp_file.h"
void sig_dfl(void);
-void end_job(cl_t *line, int status, FILE *mailf, short mailpos);
+void end_job(cl_t *line, int status, FILE *mailf, short mailpos, char **sendmailenv);
void end_mailer(cl_t *line, int status);
#ifdef HAVE_LIBPAM
--void die_mail_pame(cl_t *cl, int pamerrno, struct passwd *pas, char *str);
++void die_mail_pame(cl_t *cl, int pamerrno, struct passwd *pas, char *str, env_list_t *env);
#endif
#define PIPE_READ 0
#define PIPE_WRITE 1
int read_write_pipe(int fd, void *buf, size_t size, int action);
int read_pipe(int fd, void *to, size_t size);
int write_pipe(int fd, void *buf, size_t size);
-
-#ifndef HAVE_SETENV
-char env_user[PATH_LEN];
-char env_logname[PATH_LEN];
-char env_home[PATH_LEN];
-char env_shell[PATH_LEN];
-char env_tz[PATH_LEN];
-#endif
++void become_user(struct cl_t *cl, struct passwd *pas, char *home);
#ifdef WITH_SELINUX
extern char **environ;
{
char buf[MAX_MSG];
- strncpy(buf, str, sizeof(buf)-1);
- strncat(buf, " for '%s'", sizeof(buf)-strlen(buf)-1);
- buf[sizeof(buf)-1]='\0';
+ snprintf(buf, sizeof(buf), "%s for user '%s'", str, pas->pw_name);
if (is_mail(cl->cl_option)) {
- FILE *mailf = create_mail(cl, "Could not run fcron job");
+ char **envp = env_list_export_envp(env);
- FILE *mailf = create_mail(cl, "Could not run fcron job", NULL, envp);
++ FILE *mailf = create_mail(cl, "Could not run fcron job", envp);
/* print the error in both syslog and a file, in order to mail it to user */
if (dup2(fileno(mailf), 1) != 1 || dup2(1, 2) != 2)
pam_end(pamh, pamerrno);
- become_user(cl, pas, "/")
- /* Change running state to the user in question : it's safer to run the mail
- * as user, not root */
- if (initgroups(pas->pw_name, pas->pw_gid) < 0)
- die_e("initgroups failed: %s", pas->pw_name);
- if (setgid(pas->pw_gid) < 0)
- die("setgid failed: %s %d", pas->pw_name, pas->pw_gid);
- if (setuid(pas->pw_uid) < 0)
- die("setuid failed: %s %d", pas->pw_name, pas->pw_uid);
++ become_user(cl, pas, "/");
- launch_mailer(cl, mailf);
+ launch_mailer(cl, mailf, envp);
/* launch_mailer() does not return : we never get here */
}
else
}
#endif
-int
-change_user(struct cl_t *cl)
+void
+become_user(struct cl_t *cl, struct passwd *pas, char *home)
+/* Become the user who owns the job: change privileges, check PAM authorization,
+ * and change dir to HOME. */
{
- struct passwd *pas;
+
+#ifndef RUN_NON_PRIVILEGED
+ if (pas == NULL)
+ die("become_user() called with a NULL struct passwd");
+
+ /* Change running state to the user in question */
+ if (initgroups(pas->pw_name, pas->pw_gid) < 0)
+ die_e("initgroups failed: %s", pas->pw_name);
+
+ if (setgid(pas->pw_gid) < 0)
+ die("setgid failed: %s %d", pas->pw_name, pas->pw_gid);
+
+ if (setuid(pas->pw_uid) < 0)
+ die("setuid failed: %s %d", pas->pw_name, pas->pw_uid);
+#endif /* not RUN_NON_PRIVILEGED */
+
+ /* make sure HOME is defined and change dir to it */
+ if (chdir(home) != 0) {
+ error_e("Could not chdir to HOME dir '%s'. Trying to chdir to '/'.", home);
+ if (chdir("/") < 0)
+ die_e("Could not chdir to HOME dir /");
+ }
+
+}
+
+void
+setup_user_and_env(struct cl_t *cl, struct passwd *pas,
+ char ***sendmailenv, char ***jobenv, char **curshell, char **curhome)
+/* Check PAM authorization, and setup the environment variables
+ * to run sendmail and to run the job itself. Change dir to HOME and check if SHELL is ok */
+/* (*curshell) and (*curhome) will be allocated and should thus be freed
+ * if curshell and curhome are not NULL. */
+/* Return the the two env var sets, the shell to use to execle() commands and the home dir */
+
+{
+ env_list_t *env_list = env_list_init();
+ env_t *e = NULL;
+ char *path = NULL;
+ char *myshell = NULL;
#ifdef HAVE_LIBPAM
int retcode = 0;
-- const char * const * env;
++ char **env;
#endif
- /* Obtain password entry and change privileges */
+ if (pas == NULL)
+ die("setup_user_and_env() called with a NULL struct passwd");
+
+ env_list_setenv(env_list, "USER", pas->pw_name, 1);
+ env_list_setenv(env_list, "LOGNAME", pas->pw_name, 1);
+ env_list_setenv(env_list, "HOME", pas->pw_dir, 1);
+ /* inherit fcron's PATH for sendmail. We will later change it to DEFAULT_JOB_PATH
+ * or a user defined PATH for the job itself */
+ path = getenv("PATH");
+ env_list_setenv(env_list, "PATH", ( path != NULL ) ? path : DEFAULT_JOB_PATH, 1);
- errno = 0;
- if ((pas = getpwnam(cl->cl_runas)) == NULL)
- die_e("failed to get passwd fields for user \"%s\"", cl->cl_runas);
-
-#ifdef HAVE_SETENV
- setenv("USER", pas->pw_name, 1);
- setenv("LOGNAME", pas->pw_name, 1);
- setenv("HOME", pas->pw_dir, 1);
if (cl->cl_tz != NULL)
- setenv("TZ", cl->cl_tz, 1);
+ env_list_setenv(env_list, "TZ", cl->cl_tz, 1);
/* To ensure compatibility with Vixie cron, we don't use the shell defined
* in /etc/passwd by default, but the default value from fcron.conf instead: */
- if ( *shell == '\0' )
- /* shell is empty, ie. not defined: use value from /etc/passwd */
- setenv("SHELL", pas->pw_shell, 1);
+ if ( shell != NULL && shell[0] != '\0' )
+ /* default: use value from fcron.conf */
+ env_list_setenv(env_list, "SHELL", shell, 1);
else
- /* default: use value from fcron.conf */
- setenv("SHELL", shell, 1);
-#else
- {
- strcpy(env_user, "USER=");
- strncat(env_user, pas->pw_name, sizeof(env_user)-5-1);
- env_user[sizeof(env_user)-1]='\0';
- putenv( env_user );
-
- strcpy(env_logname, "LOGNAME=");
- strncat(env_logname, pas->pw_name, sizeof(env_logname)-8-1);
- env_logname[sizeof(env_logname)-1]='\0';
- putenv( env_logname );
-
- strcpy(env_home, "HOME=");
- strncat(env_home, pas->pw_dir, sizeof(env_home)-5-1);
- env_home[sizeof(env_home)-1]='\0';
- putenv( env_home );
-
- if (cl->cl_tz != NULL) {
- strcpy(env_tz, "TZ=");
- strncat(env_tz, pas->pw_dir, sizeof(env_tz)-3-1);
- env_tz[sizeof(env_tz)-1]='\0';
- putenv( env_tz );
- }
+ /* shell is empty, ie. not defined: fail back to /etc/passwd's value */
+ env_list_setenv(env_list, "SHELL", pas->pw_shell, 1);
- strcpy(env_shell, "SHELL=");
- /* To ensure compatibility with Vixie cron, we don't use the shell defined
- * in /etc/passwd by default, but the default value from fcron.conf instead: */
- if ( *shell == '\0' )
- /* shell is empty, ie. not defined: use value from /etc/passwd */
- strncat(env_shell, pas->pw_shell, sizeof(env_shell)-6-1);
- else
- /* default: use value from fcron.conf */
- strncat(env_shell, shell, sizeof(env_shell)-6-1);
- env_shell[sizeof(env_shell)-1]='\0';
- putenv( env_shell );
- }
-#endif /* HAVE_SETENV */
-
-#ifdef HAVE_LIBPAM
+#if ( ! defined(RUN_NON_PRIVILEGED)) && defined(HAVE_LIBPAM)
/* Open PAM session for the user and obtain any security
credentials we might need */
* we must set auth to pam_permit. */
retcode = pam_authenticate(pamh, PAM_SILENT);
if (retcode != PAM_SUCCESS) die_mail_pame(cl, retcode, pas,
- "Could not authenticate PAM user");
+ "Could not authenticate PAM user", env_list);
retcode = pam_acct_mgmt(pamh, PAM_SILENT); /* permitted access? */
if (retcode != PAM_SUCCESS) die_mail_pame(cl, retcode, pas,
- "Could not init PAM account management");
+ "Could not init PAM account management", env_list);
retcode = pam_setcred(pamh, PAM_ESTABLISH_CRED | PAM_SILENT);
if (retcode != PAM_SUCCESS) die_mail_pame(cl, retcode, pas,
- "Could not set PAM credentials");
+ "Could not set PAM credentials", env_list);
retcode = pam_open_session(pamh, PAM_SILENT);
if (retcode != PAM_SUCCESS) die_mail_pame(cl, retcode, pas,
- "Could not open PAM session");
+ "Could not open PAM session", env_list);
- for (env = (const char * const *)pam_getenvlist(pamh); env && *env; env++) {
- env = (const char * const *) pam_getenvlist(pamh);
- while (env && *env) {
- if (putenv((char*) *env)) die_e("Could not copy PAM environment");
- env++;
++ for (env = pam_getenvlist(pamh); env && *env; env++) {
+ env_list_putenv(env_list, *env, 1);
}
/* Close the log here, because PAM calls openlog(3) and
}
- * make it as it would be if we had seteuid()
+ #ifdef USE_SETE_ID
+ void
+ seteuid_safe(uid_t euid)
+ /* set the euid if different from the current one, and die on error */
+ {
+ /* on BSD, one can only seteuid() to the real UID or saved UID,
+ * and NOT the euid, so we don't call seteuid(geteuid()),
+ * which is why we need to check if a change is needed */
+
+ if (geteuid() != euid && seteuid(euid) != 0)
+ die_e("could not change euid to %d", euid);
+
+ }
+
+ void
+ setegid_safe(gid_t egid)
+ /* set the egid if different from the current one, and die on error */
+ {
+ /* on BSD, one can only setegid() to the real GID or saved GID,
+ * and NOT the egid, so we don't call setegid(getegid()),
+ * which is why we need to check if a change is needed */
+
+ if (getegid() != egid && setegid(egid) != 0)
+ die_e("could not change egid to %d", egid);
+
+ }
+ #endif /* def USE_SETE_ID */
+
+ #ifdef USE_SETE_ID
+ int
+ open_as_user(const char *pathname, uid_t openuid, gid_t opengid, int flags, ...)
+ /* Become user and call open(), then revert back to who we were.
+ * NOTE: when flags & O_CREAT, the 5th argument is mode_t and must be set
+ * -- it is ignored otherwise */
+ {
+ uid_t orig_euid = geteuid();
+ gid_t orig_egid = getegid();
+ struct stat s;
+ int fd = -1;
+ va_list ap;
+ mode_t mode = (mode_t) 0;
+
+ if (flags & O_CREAT) {
+ va_start(ap, flags);
+ mode = va_arg(ap, mode_t);
+ va_end(ap);
+ }
+
+ seteuid_safe(openuid);
+ setegid_safe(opengid);
+
+ if (flags & O_CREAT) {
+ fd = open(pathname, flags, mode);
+ }
+ else
+ fd = open(pathname, flags);
+
+ /* change the effective uid/gid back to original values */
+ seteuid_safe(orig_euid);
+ setegid_safe(orig_egid);
+
+ /* if open() didn't fail make sure we opened a 'normal' file */
+ if ( fd >= 0 ) {
+
+ if ( fstat(fd, &s) < 0 ) {
+ error_e("open_as_user(): could not fstat %s", pathname);
+ if ( close(fd) < 0 )
+ error_e("open_as_user: could not close() %s", pathname);
+ fd = -1;
+ }
+
+ if ( ! S_ISREG(s.st_mode) || s.st_nlink != 1 ) {
+ error_e("open_as_user(): file %s is not a regular file", pathname);
+ if ( close(fd) < 0 )
+ error_e("open_as_user: could not close() %s", pathname);
+ errno = 0;
+ fd = -1;
+ }
+
+ }
+
+ return fd;
+
+ }
+
+ #else /* def USE_SETE_ID */
+
+ int
+ open_as_user(const char *pathname, uid_t openuid, gid_t opengid, int flags, ...)
+ /* Become user and call open(), then revert back to who we were.
+ * As seteuid() is not available on this system attempt to similate that behavior
+ * as closely as possible.
+ * NOTE: when flags & O_CREAT, the 5th argument is mode_t and must be set
+ * -- it is ignored otherwise */
+ {
+ int fd = -1;
+ struct stat s;
+ va_list ap;
+ mode_t mode = (mode_t) 0;
+
+ if (flags & O_CREAT) {
+ va_start(ap, flags);
+ mode = va_arg(ap, mode_t);
+ va_end(ap);
+ }
+
+ /* In case a flag as O_TRUNC is set, we should test if the user
+ * is allowed to open the file before we open it.
+ * There will always be a risk of race-condition between the test
+ * and the open but that's the best we can realistically do
+ * without seteuid()... */
+ if ( stat(pathname, &s) == 0 ) {
+ if ( ! ( s.st_mode & S_IROTH
+ || ( s.st_uid == openuid && s.st_mode & S_IRUSR )
+ || ( s.st_gid == opengid && s.st_mode & S_IRGRP ) ) ) {
+ error("open_as_user(): file %s does not pass the security test: "
+ "uid=%d gid=%d mode=%lo openuid=%d opengid=%d",
+ pathname, s.st_uid, s.st_gid, s.st_mode, openuid, opengid);
+ errno = EACCES;
+ return -1;
+ }
+ }
+ else if ( errno == ENOENT ) {
+ /* the file doesn't exist so no risk to truncate the wrong file! */
+ ;
+ }
+ else {
+ error_e("open_as_user(): could not stat %s", pathname);
+ return -1;
+ }
+
+ if (flags & O_CREAT) {
+ fd = open(pathname, flags, mode);
+ }
+ else
+ fd = open(pathname, flags);
+
+ if ( fd < 0 )
+ /* we couldn't open the file */
+ return fd;
+
+ /* if open() didn't fail make sure we opened a 'normal' file */
+ if ( fstat(fd, &s) < 0 ) {
+ error_e("open_as_user(): could not fstat %s", pathname);
+ goto err;
+ }
+ if ( ! S_ISREG(s.st_mode) || s.st_nlink != 1 ) {
+ error_e("open_as_user(): file %s is not a regular file", pathname);
+ goto err;
+ }
+
+ /* we couldn't become openuid/opengid, so check manually if the user
+ * is allowed to read that file
+ * We do that again as a malicious user could have replaced the file
+ * by another one (e.g. a link) between the stat() and the open() earlier */
+ if ( ! ( s.st_mode & S_IROTH
+ || ( s.st_uid == openuid && s.st_mode & S_IRUSR )
+ || ( s.st_gid == opengid && s.st_mode & S_IRGRP ) ) ) {
+ error("open_as_user(): file %s does not pass the security test: "
+ "uid=%d gid=%d mode=%lo openuid=%d opengid=%d",
+ pathname, s.st_uid, s.st_gid, s.st_mode, openuid, opengid);
+ errno = EACCES;
+ goto err;
+ }
+
+ /* if we created a new file, change the file ownership:
++ * make it as it would be if we had seteuid()
+ * NOTE: if O_CREAT was set without O_EXCL and the file existed before
+ * then we will end up changing the ownership even if the seteuid()
+ * version of that function wouldn't have. That shouldn't break
+ * anything though. */
+ if ( (flags & O_CREAT) && fchown(fd, openuid, opengid) != 0) {
+ error_e("Could not fchown %s to uid:%d gid:%d", pathname, openuid, opengid);
+ if ( close(fd) < 0 )
+ error_e("open_as_user: could not close() %s", pathname);
+ return -1;
+ }
+
+ /* everything went ok: return the file descriptor */
+ return fd;
+
+ err:
+ if ( fd >= 0 && close(fd) < 0 )
+ error_e("open_as_user: could not close() %s", pathname);
+ return -1;
+ }
+
+ #endif /* def USE_SETE_ID */
+
+ int
+ remove_as_user(const char *pathname, uid_t removeuid, gid_t removegid)
+ /* Become user and call remove(), then revert back to who we were */
+ {
+ int rval = -1;
+ #ifdef USE_SETE_ID
+ uid_t orig_euid = geteuid();
+ gid_t orig_egid = getegid();
+
+ seteuid_safe(removeuid);
+ setegid_safe(removegid);
+ #endif /* def USE_SETE_ID */
+
+ rval = remove(pathname);
+
+ #ifdef USE_SETE_ID
+ seteuid_safe(orig_euid);
+ setegid_safe(orig_egid);
+ #endif /* def USE_SETE_ID */
+
+ return rval;
+ }
+
+ int
+ rename_as_user(const char *oldpath, const char *newpath, uid_t renameuid, gid_t renamegid)
+ /* Become user and call rename(), then revert back to who we were */
+ {
+ int rval = -1;
+ #ifdef USE_SETE_ID
+ uid_t orig_euid = geteuid();
+ gid_t orig_egid = getegid();
+
+ seteuid_safe(renameuid);
+ setegid_safe(renamegid);
+ #endif /* def USE_SETE_ID */
+
+ rval = rename(oldpath, newpath);
+
+ #ifdef USE_SETE_ID
+ seteuid_safe(orig_euid);
+ setegid_safe(orig_egid);
+ #endif /* def USE_SETE_ID */
+
+ return rval;
+
+ }
+
int
remove_blanks(char *str)
/* remove blanks at the the end of str */
/* functions prototypes */
extern uid_t get_user_uid_safe(char *username);
extern gid_t get_group_gid_safe(char *groupname);
+ extern void seteuid_safe(uid_t euid);
+ extern void setegid_safe(uid_t egid);
+ extern int remove_as_user(const char *pathname, uid_t removeuid, gid_t removegid);
+ extern int open_as_user(const char *pathname, uid_t openuid, gid_t opengid, int flags, ...);
+ extern int rename_as_user(const char *oldpath, const char *newpath, uid_t renameuid, gid_t renamegid);
+
extern int remove_blanks(char *str);
+extern int strcmp_until(const char *left, const char *right, char until);
extern char *strdup2(const char *);
+extern void *alloc_safe(size_t len, const char * desc);
+extern void *realloc_safe(void *ptr, size_t len, const char * desc);
+extern void free_safe(void *ptr);
extern int get_word(char **str);
-extern int temp_file(char **name);
-extern void read_conf(void);
-extern void free_conf(void);
+extern void my_unsetenv(const char *name);
+extern void my_setenv_overwrite(const char *name, const char *value);
#endif /* __SUBS_H__ */