s4-pvfs: use O_FOLLOW one level at a time for security overrides
authorAndrew Tridgell <tridge@samba.org>
Fri, 5 Mar 2010 11:46:44 +0000 (22:46 +1100)
committerAndrew Tridgell <tridge@samba.org>
Fri, 5 Mar 2010 12:07:32 +0000 (23:07 +1100)
To prevent symlink attacks we need to use O_NOFOLLOW one level at a
time when processing a root security override

source4/ntvfs/posix/pvfs_sys.c

index 9dfe6eef1a005684c4bebf9daa2cfc384da9c374..5265e25007491c7416b5505d9fb0a5ff93957444 100644 (file)
   has already been checked. The wrappers will override a EACCES result
   by gaining root privileges if the 'pvfs:perm override' is set on the
   share (it is enabled by default)
+
+  Careful use of O_NOFOLLOW and O_DIRECTORY is used to prevent
+  security attacks via symlinks
+ */
+
+
+struct pvfs_sys_ctx {
+       struct pvfs_state *pvfs;
+       void *privs;
+       const char *old_wd;
+       struct stat st_orig;
+};
+
+/*
+  return to original directory when context is destroyed
+ */
+static int pvfs_sys_pushdir_destructor(struct pvfs_sys_ctx *ctx)
+{
+       struct stat st;
+
+       if (ctx->old_wd == NULL) {
+               return 0;
+       }
+
+       if (chdir(ctx->old_wd) != 0) {
+               smb_panic("Failed to restore working directory");
+       }
+       if (stat(".", &st) != 0) {
+               smb_panic("Failed to stat working directory");
+       }
+       if (st.st_ino != ctx->st_orig.st_ino ||
+           st.st_dev != ctx->st_orig.st_dev) {
+               smb_panic("Working directory changed during call");
+       }
+
+       return 0;
+}
+
+
+/*
+  chdir() to the directory part of a pathname, but disallow any
+  component with a symlink
+
+  Note that we can't use O_NOFOLLOW on the whole path as that only
+  prevents links in the final component of the path
+ */
+static int pvfs_sys_chdir_nosymlink(struct pvfs_sys_ctx *ctx, const char *pathname)
+{
+       char *p, *path;
+       size_t base_len = strlen(ctx->pvfs->base_directory);
+
+       /* don't check for symlinks in the base directory of the share */
+       if (strncmp(ctx->pvfs->base_directory, pathname, base_len) == 0 &&
+           pathname[base_len] == '/') {
+               if (chdir(ctx->pvfs->base_directory) != 0) {
+                       return -1;
+               }
+               pathname += base_len + 1;
+       }
+
+       path = talloc_strdup(ctx, pathname);
+       if (path == NULL) {
+               return -1;
+       }
+       while ((p = strchr(path, '/'))) {
+               int fd;
+               struct stat st1, st2;
+               *p = 0;
+               fd = open(path, O_NOFOLLOW | O_DIRECTORY | O_RDONLY);
+               if (fd == -1) {
+                       return -1;
+               }
+               if (chdir(path) != 0) {
+                       close(fd);
+                       return -1;
+               }
+               if (stat(".", &st1) != 0 ||
+                   fstat(fd, &st2) != 0) {
+                       close(fd);
+                       return -1;
+               }
+               close(fd);
+               if (st1.st_ino != st2.st_ino ||
+                   st1.st_dev != st2.st_dev) {
+                       DEBUG(0,(__location__ ": Inode changed during chdir in '%s' - symlink attack?",
+                                pathname));
+                       return -1;
+               }
+               path = p + 1;
+       }
+
+       return 0;
+}
+
+
+/*
+  become root, and change directory to the directory component of a
+  path. Return a talloc context which when freed will move us back
+  to the original directory, and return us to the original uid
+
+  change the pathname argument to contain just the base component of
+  the path
+
+  return NULL on error, which could include an attempt to subvert
+  security using symlink tricks
  */
+static struct pvfs_sys_ctx *pvfs_sys_pushdir(struct pvfs_state *pvfs,
+                                            const char **pathname)
+{
+       struct pvfs_sys_ctx *ctx;
+       char *cwd, *p, *dirname;
+       int ret;
+
+       ctx = talloc_zero(pvfs, struct pvfs_sys_ctx);
+       if (ctx == NULL) {
+               return NULL;
+       }
+       ctx->pvfs = pvfs;
+       ctx->privs = root_privileges();
+       if (ctx->privs == NULL) {
+               talloc_free(ctx);
+               return NULL;
+       }
+
+       talloc_steal(ctx, ctx->privs);
+
+       if (!pathname) {
+               /* no pathname needed */
+               return ctx;
+       }
+
+       p = strrchr(*pathname, '/');
+       if (p == NULL) {
+               /* we don't need to change directory */
+               return ctx;
+       }
+
+       /* we keep the old st around, so we can tell that
+          we have come back to the right directory */
+       if (stat(".", &ctx->st_orig) != 0) {
+               talloc_free(ctx);
+               return NULL;
+       }
+
+       cwd = get_current_dir_name();
+       if (cwd == NULL) {
+               talloc_free(ctx);
+               return NULL;
+       }
+       ctx->old_wd = talloc_strdup(ctx, cwd);
+       if (ctx->old_wd == NULL) {
+               free(cwd);
+               talloc_free(ctx);
+               return NULL;
+       }
+
+       dirname = talloc_strndup(ctx, *pathname, (p - *pathname));
+       if (dirname == NULL) {
+               talloc_free(ctx);
+               return NULL;
+       }
+
+       ret = pvfs_sys_chdir_nosymlink(ctx, *pathname);
+       if (ret == -1) {
+               talloc_free(ctx);
+               return NULL;
+       }
+
+       talloc_set_destructor(ctx, pvfs_sys_pushdir_destructor);
+
+       /* return the basename as the filename that should be operated on */
+       (*pathname) = talloc_strdup(ctx, p+1);
+       if (! *pathname) {
+               talloc_free(ctx);
+               return NULL;
+       }
+
+       return ctx;
+}
 
 
 /*
   chown a file that we created with a root privileges override
  */
-static int pvfs_sys_fchown(struct pvfs_state *pvfs, void *privs, int fd)
+static int pvfs_sys_fchown(struct pvfs_state *pvfs, struct pvfs_sys_ctx *ctx, int fd)
 {
-       return fchown(fd, root_privileges_original_uid(privs), -1);
+       return fchown(fd, root_privileges_original_uid(ctx->privs), -1);
 }
 
 /*
   chown a directory that we created with a root privileges override
  */
-static int pvfs_sys_chown(struct pvfs_state *pvfs, void *privs, const char *name)
+static int pvfs_sys_chown(struct pvfs_state *pvfs, struct pvfs_sys_ctx *ctx, const char *name)
 {
-       return chown(name, root_privileges_original_uid(privs), -1);
+       /* to avoid symlink hacks, we need to use fchown() on a directory fd */
+       int ret, fd;
+       fd = open(name, O_DIRECTORY | O_NOFOLLOW | O_RDONLY);
+       if (fd == -1) {
+               return -1;
+       }
+       ret = pvfs_sys_fchown(pvfs, ctx, fd);
+       close(fd);
+       return ret;
 }
 
 
@@ -55,7 +241,7 @@ static int pvfs_sys_chown(struct pvfs_state *pvfs, void *privs, const char *name
 int pvfs_sys_open(struct pvfs_state *pvfs, const char *filename, int flags, mode_t mode)
 {
        int fd, ret;
-       void *privs;
+       struct pvfs_sys_ctx *ctx;
        int saved_errno, orig_errno;
        int retries = 5;
 
@@ -69,12 +255,14 @@ int pvfs_sys_open(struct pvfs_state *pvfs, const char *filename, int flags, mode
        }
 
        saved_errno = errno;
-       privs = root_privileges();
+       ctx = pvfs_sys_pushdir(pvfs, &filename);
+       if (ctx == NULL) {
+               errno = saved_errno;
+               return -1;
+       }
 
        /* don't allow permission overrides to follow links */
-#ifdef O_NOFOLLOW
        flags |= O_NOFOLLOW;
-#endif
 
        /*
           if O_CREAT was specified and O_EXCL was not specified
@@ -89,35 +277,35 @@ int pvfs_sys_open(struct pvfs_state *pvfs, const char *filename, int flags, mode
                   with anything other than ENOENT, then we return the
                   open result, with the original errno */
                if (fd == -1 && errno != ENOENT) {
-                       talloc_free(privs);
+                       talloc_free(ctx);
                        errno = saved_errno;
                        return -1;
                }
                if (fd != -1) {
                        /* the file already existed and we opened it */
-                       talloc_free(privs);
+                       talloc_free(ctx);
                        errno = orig_errno;
                        return fd;
                }
 
                fd = open(filename, flags | O_EXCL, mode);
                if (fd == -1 && errno != EEXIST) {
-                       talloc_free(privs);
+                       talloc_free(ctx);
                        errno = saved_errno;
                        return -1;
                }
                if (fd != -1) {
                        /* we created the file, we need to set the
                           right ownership on it */
-                       ret = pvfs_sys_fchown(pvfs, privs, fd);
+                       ret = pvfs_sys_fchown(pvfs, ctx, fd);
                        if (ret == -1) {
                                close(fd);
                                unlink(filename);
-                               talloc_free(privs);
+                               talloc_free(ctx);
                                errno = saved_errno;
                                return -1;
                        }
-                       talloc_free(privs);
+                       talloc_free(ctx);
                        errno = orig_errno;
                        return fd;
                }
@@ -128,31 +316,31 @@ int pvfs_sys_open(struct pvfs_state *pvfs, const char *filename, int flags, mode
                        goto try_again;
                }
 
-               talloc_free(privs);
+               talloc_free(ctx);
                errno = saved_errno;
                return -1;
        }
 
        fd = open(filename, flags, mode);
        if (fd == -1) {
-               talloc_free(privs);
+               talloc_free(ctx);
                errno = saved_errno;
                return -1;
        }
 
        /* if we have created a file then fchown it */
        if (flags & O_CREAT) {
-               ret = pvfs_sys_fchown(pvfs, privs, fd);
+               ret = pvfs_sys_fchown(pvfs, ctx, fd);
                if (ret == -1) {
                        close(fd);
                        unlink(filename);
-                       talloc_free(privs);
+                       talloc_free(ctx);
                        errno = saved_errno;
                        return -1;
                }
        }
 
-       talloc_free(privs);
+       talloc_free(ctx);
        return fd;
 }
 
@@ -163,7 +351,7 @@ int pvfs_sys_open(struct pvfs_state *pvfs, const char *filename, int flags, mode
 int pvfs_sys_unlink(struct pvfs_state *pvfs, const char *filename)
 {
        int ret;
-       void *privs;
+       struct pvfs_sys_ctx *ctx;
        int saved_errno, orig_errno;
 
        orig_errno = errno;
@@ -177,27 +365,42 @@ int pvfs_sys_unlink(struct pvfs_state *pvfs, const char *filename)
 
        saved_errno = errno;
 
-       privs = root_privileges();
+       ctx = pvfs_sys_pushdir(pvfs, &filename);
+       if (ctx == NULL) {
+               errno = saved_errno;
+               return -1;
+       }
+
        ret = unlink(filename);
        if (ret == -1) {
                errno = saved_errno;
-               talloc_free(privs);
+               talloc_free(ctx);
                return -1;
        }
 
        errno = orig_errno;
-       talloc_free(privs);
+       talloc_free(ctx);
        return ret;
 }
 
 
+static bool contains_symlink(const char *path)
+{
+       int fd = open(path, O_NOFOLLOW | O_RDONLY);
+       if (fd != -1) {
+               close(fd);
+               return false;
+       }
+       return (errno == ELOOP);
+}
+
 /*
   wrap rename for system override
 */
 int pvfs_sys_rename(struct pvfs_state *pvfs, const char *name1, const char *name2)
 {
        int ret;
-       void *privs;
+       struct pvfs_sys_ctx *ctx;
        int saved_errno, orig_errno;
 
        orig_errno = errno;
@@ -211,16 +414,47 @@ int pvfs_sys_rename(struct pvfs_state *pvfs, const char *name1, const char *name
 
        saved_errno = errno;
 
-       privs = root_privileges();
+       ctx = pvfs_sys_pushdir(pvfs, &name1);
+       if (ctx == NULL) {
+               errno = saved_errno;
+               return -1;
+       }
+
+       /* we need the destination as an absolute path */
+       if (name2[0] != '/') {
+               name2 = talloc_asprintf(ctx, "%s/%s", ctx->old_wd, name2);
+               if (name2 == NULL) {
+                       errno = saved_errno;
+                       talloc_free(ctx);
+                       return -1;
+               }
+       }
+
+       /* make sure the destination isn't a symlink beforehand */
+       if (contains_symlink(name2)) {
+               errno = saved_errno;
+               talloc_free(ctx);
+               return -1;
+       }
+
        ret = rename(name1, name2);
        if (ret == -1) {
                errno = saved_errno;
-               talloc_free(privs);
+               talloc_free(ctx);
+               return -1;
+       }
+
+       /* make sure the destination isn't a symlink afterwards */
+       if (contains_symlink(name2)) {
+               DEBUG(0,(__location__ ": Possible symlink attack in rename to '%s' - unlinking\n", name2));
+               unlink(name2);
+               errno = saved_errno;
+               talloc_free(ctx);
                return -1;
        }
 
        errno = orig_errno;
-       talloc_free(privs);
+       talloc_free(ctx);
        return ret;
 }
 
@@ -231,7 +465,7 @@ int pvfs_sys_rename(struct pvfs_state *pvfs, const char *name1, const char *name
 int pvfs_sys_mkdir(struct pvfs_state *pvfs, const char *dirname, mode_t mode)
 {
        int ret;
-       void *privs;
+       struct pvfs_sys_ctx *ctx;
        int saved_errno, orig_errno;
 
        orig_errno = errno;
@@ -244,24 +478,28 @@ int pvfs_sys_mkdir(struct pvfs_state *pvfs, const char *dirname, mode_t mode)
        }
 
        saved_errno = errno;
-       privs = root_privileges();
+       ctx = pvfs_sys_pushdir(pvfs, &dirname);
+       if (ctx == NULL) {
+               errno = saved_errno;
+               return -1;
+       }
 
        ret = mkdir(dirname, mode);
        if (ret == -1) {
-               talloc_free(privs);
+               talloc_free(ctx);
                errno = saved_errno;
                return -1;
        }
 
-       ret = pvfs_sys_chown(pvfs, privs, dirname);
+       ret = pvfs_sys_chown(pvfs, ctx, dirname);
        if (ret == -1) {
                rmdir(dirname);
-               talloc_free(privs);
+               talloc_free(ctx);
                errno = saved_errno;
                return -1;
        }
 
-       talloc_free(privs);
+       talloc_free(ctx);
        return ret;
 }
 
@@ -272,7 +510,7 @@ int pvfs_sys_mkdir(struct pvfs_state *pvfs, const char *dirname, mode_t mode)
 int pvfs_sys_rmdir(struct pvfs_state *pvfs, const char *dirname)
 {
        int ret;
-       void *privs;
+       struct pvfs_sys_ctx *ctx;
        int saved_errno, orig_errno;
 
        orig_errno = errno;
@@ -286,15 +524,97 @@ int pvfs_sys_rmdir(struct pvfs_state *pvfs, const char *dirname)
 
        saved_errno = errno;
 
-       privs = root_privileges();
+       ctx = pvfs_sys_pushdir(pvfs, &dirname);
+       if (ctx == NULL) {
+               errno = saved_errno;
+               return -1;
+       }
+
        ret = rmdir(dirname);
        if (ret == -1) {
                errno = saved_errno;
-               talloc_free(privs);
+               talloc_free(ctx);
+               return -1;
+       }
+
+       errno = orig_errno;
+       talloc_free(ctx);
+       return ret;
+}
+
+/*
+  wrap fchmod for system override
+*/
+int pvfs_sys_fchmod(struct pvfs_state *pvfs, int fd, mode_t mode)
+{
+       int ret;
+       struct pvfs_sys_ctx *ctx;
+       int saved_errno, orig_errno;
+
+       orig_errno = errno;
+
+       ret = fchmod(fd, mode);
+       if (ret != -1 ||
+           !(pvfs->flags & PVFS_FLAG_PERM_OVERRIDE) ||
+           errno != EACCES) {
+               return ret;
+       }
+
+       saved_errno = errno;
+
+       ctx = pvfs_sys_pushdir(pvfs, NULL);
+       if (ctx == NULL) {
+               errno = saved_errno;
+               return -1;
+       }
+
+       ret = fchmod(fd, mode);
+       if (ret == -1) {
+               errno = saved_errno;
+               talloc_free(ctx);
+               return -1;
+       }
+
+       errno = orig_errno;
+       talloc_free(ctx);
+       return ret;
+}
+
+
+/*
+  wrap chmod for system override
+*/
+int pvfs_sys_chmod(struct pvfs_state *pvfs, const char *filename, mode_t mode)
+{
+       int ret;
+       struct pvfs_sys_ctx *ctx;
+       int saved_errno, orig_errno;
+
+       orig_errno = errno;
+
+       ret = chmod(filename, mode);
+       if (ret != -1 ||
+           !(pvfs->flags & PVFS_FLAG_PERM_OVERRIDE) ||
+           errno != EACCES) {
+               return ret;
+       }
+
+       saved_errno = errno;
+
+       ctx = pvfs_sys_pushdir(pvfs, &filename);
+       if (ctx == NULL) {
+               errno = saved_errno;
+               return -1;
+       }
+
+       ret = chmod(filename, mode);
+       if (ret == -1) {
+               errno = saved_errno;
+               talloc_free(ctx);
                return -1;
        }
 
        errno = orig_errno;
-       talloc_free(privs);
+       talloc_free(ctx);
        return ret;
 }