From: Thibault Godouet Date: Fri, 24 Sep 2010 15:09:36 +0000 (+0100) Subject: Merge branch 'fcron-3.0' X-Git-Tag: ver3_1_0~34 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=4e22ddd96f2ed185b06c8db33fbe52f3448e7748;p=fcron Merge branch 'fcron-3.0' Fixed some typos related to env var in job.c Conflicts: configure configure.in doc/en/todo.sgml fcrondyn.c fcrontab.c read_string.c subs.c --- 4e22ddd96f2ed185b06c8db33fbe52f3448e7748 diff --cc doc/en/changes.sgml index fb44b26,cfbaaea..2e59c23 --- a/doc/en/changes.sgml +++ b/doc/en/changes.sgml @@@ -13,18 -13,14 +13,24 @@@ A copy of the license is included in gf Changes + + From version 3.0.5 to 3.1.0 + + Pass fcrondyn client credentials through the socket when possible so as the user doesn't need to type his password when using fcrondyn. + + + Code clean-up: Implemented generic unordred list for lavgq / exeq. + + + From version 3.0.4 to 3.0.5 + + 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. + + + Improved general security of fcrontab and fcrondyn by dropping more privileges. + Work on portability (aix, hpux, irix, tru64 unix, solaris). Thanks Peter O'Gorman for his patch! diff --cc doc/fr/todo.sgml index 4182be6,f302ff7..507d5e7 --- a/doc/fr/todo.sgml +++ b/doc/fr/todo.sgml @@@ -126,4 -126,4 +126,3 @@@ mode: sgm sgml-parent-document:("fcron-doc.sgml" "book" "chapter" "sect1" "") End: --> -- diff --cc fcrontab.c index 3645137,cb265bd..fb905eb --- a/fcrontab.c +++ b/fcrontab.c @@@ -645,11 -655,14 +649,14 @@@ install_stdin(void 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; @@@ -991,7 -1004,14 +998,14 @@@ main(int argc, char **argv 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)) ) @@@ -1015,6 -1028,11 +1022,11 @@@ parseopt(argc, argv); #ifdef USE_SETE_ID + /* drop any privilege we may have: we will only get them back + * temporarily every time we need it. */ - seteuid_safe(useruid); - setegid_safe(usergid); ++ seteuid_safe(useruid); ++ setegid_safe(usergid); + #endif #ifdef HAVE_LIBPAM /* Open PAM session for the user and obtain any security diff --cc fileconf.c index 062f39c,574fdc8..fa2d233 --- a/fileconf.c +++ b/fileconf.c @@@ -150,21 -147,8 +150,21 @@@ get_line(char *str, size_t size, FILE * } +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; @@@ -192,10 -174,16 +189,13 @@@ 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); @@@ -278,11 -264,13 +278,13 @@@ 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; diff --cc job.c index 5815609,b748b49..e7f1b8f --- a/job.c +++ b/job.c @@@ -26,19 -26,26 +26,20 @@@ #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; @@@ -51,11 -58,12 +52,11 @@@ die_mail_pame(cl_t *cl, int pamerrno, s { 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) @@@ -68,9 -76,16 +69,9 @@@ 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 @@@ -78,78 -93,74 +79,78 @@@ } #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 */ @@@ -161,19 -172,21 +162,19 @@@ * 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 diff --cc subs.c index 4a79dd2,83949e6..151bd51 --- a/subs.c +++ b/subs.c @@@ -59,6 -77,242 +59,242 @@@ get_group_gid_safe(char *groupname } + #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() ++ * 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 */ diff --cc subs.h index d94ca34,15e6961..e35eb42 --- a/subs.h +++ b/subs.h @@@ -30,14 -30,31 +30,20 @@@ /* 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__ */