LCOV - code coverage report
Current view: top level - source3/modules - vfs_widelinks.c (source / functions) Hit Total Coverage
Test: coverage report for master 2f515e9b Lines: 90 118 76.3 %
Date: 2024-04-21 15:09:00 Functions: 8 8 100.0 %

          Line data    Source code
       1             : /*
       2             :  * Widelinks VFS module. Causes smbd not to see symlinks.
       3             :  *
       4             :  * Copyright (C) Jeremy Allison, 2020
       5             :  *
       6             :  * This program is free software; you can redistribute it and/or modify
       7             :  * it under the terms of the GNU General Public License as published by
       8             :  * the Free Software Foundation; either version 3 of the License, or
       9             :  * (at your option) any later version.
      10             :  *
      11             :  * This program is distributed in the hope that it will be useful,
      12             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      13             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      14             :  * GNU General Public License for more details.
      15             :  *
      16             :  * You should have received a copy of the GNU General Public License
      17             :  * along with this program; if not, see <http://www.gnu.org/licenses/>.
      18             :  */
      19             : 
      20             : /*
      21             :  What does this module do ? It implements the explicitly insecure
      22             :  "widelinks = yes" functionality that used to be in the core smbd
      23             :  code.
      24             : 
      25             :  Now this is implemented here, the insecure share-escape code that
      26             :  explicitly allows escape from an exported share path can be removed
      27             :  from smbd, leaving it a cleaner and more maintainable code base.
      28             : 
      29             :  The smbd code can now always return ACCESS_DENIED if a path
      30             :  leads outside a share.
      31             : 
      32             :  How does it do that ? There are 2 features.
      33             : 
      34             :  1). When the upper layer code does a chdir() call to a pathname,
      35             :  this module stores the requested pathname inside config->cwd.
      36             : 
      37             :  When the upper layer code does a getwd() or realpath(), we return
      38             :  the absolute path of the value stored in config->cwd, *not* the
      39             :  position on the underlying filesystem.
      40             : 
      41             :  This hides symlinks as if the chdir pathname contains a symlink,
      42             :  normally doing a realpath call on it would return the real
      43             :  position on the filesystem. For widelinks = yes, this isn't what
      44             :  you want. You want the position you think is underneath the share
      45             :  definition - the symlink path you used to go outside the share,
      46             :  not the contents of the symlink itself.
      47             : 
      48             :  That way, the upper layer smbd code can strictly enforce paths
      49             :  being underneath a share definition without the knowledge that
      50             :  "widelinks = yes" has moved us outside the share definition.
      51             : 
      52             :  1a). Note that when setting up a share, smbd may make calls such
      53             :  as realpath and stat/lstat in order to set up the share definition.
      54             :  These calls are made *before* smbd calls chdir() to move the working
      55             :  directory below the exported share definition. In order to allow
      56             :  this, all the vfs_widelinks functions are coded to just pass through
      57             :  the vfs call to the next module in the chain if (a). The widelinks
      58             :  module was loaded in error by an administrator and widelinks is
      59             :  set to "no". This is the:
      60             : 
      61             :         if (!config->active) {
      62             :                 Module not active.
      63             :                 SMB_VFS_NEXT_XXXXX(...)
      64             :         }
      65             : 
      66             :  idiom in the vfs functions.
      67             : 
      68             :  1b). If the module was correctly active, but smbd has yet
      69             :  to call chdir(), then config->cwd == NULL. In that case
      70             :  the correct action (to match the previous widelinks behavior
      71             :  in the code inside smbd) is to pass through the vfs call to
      72             :  the next module in the chain. That way, any symlinks in the
      73             :  pathname are still exposed to smbd, which will restrict them to
      74             :  be under the exported share definition. This allows the module
      75             :  to "fail safe" for any vfs call made when setting up the share
      76             :  structure definition, rather than fail unsafe by hiding symlinks
      77             :  before chdir is called. This is the:
      78             : 
      79             :         if (config->cwd == NULL) {
      80             :                 XXXXX syscall before chdir - see note 1b above.
      81             :                 return SMB_VFS_NEXT_XXXXX()
      82             :         }
      83             : 
      84             :  idiom in the vfs functions.
      85             : 
      86             :  2). The module hides the existence of symlinks by inside
      87             :  lstat(), open(), and readdir() so long as it's not a POSIX
      88             :  pathname request (those requests *must* be aware of symlinks
      89             :  and the POSIX client has to follow them, it's expected that
      90             :  a server will always fail to follow symlinks).
      91             : 
      92             :  It does this by:
      93             : 
      94             :  2a). lstat -> stat
      95             :  2b). open removes any O_NOFOLLOW from flags.
      96             :  2c). The optimization in readdir that returns a stat
      97             :  struct is removed as this could return a symlink mode
      98             :  bit, causing smbd to always call stat/lstat itself on
      99             :  a pathname (which we'll then use to hide symlinks).
     100             : 
     101             : */
     102             : 
     103             : #include "includes.h"
     104             : #include "smbd/smbd.h"
     105             : #include "lib/util_path.h"
     106             : 
     107             : struct widelinks_config {
     108             :         bool active;
     109             :         bool is_dfs_share;
     110             :         char *cwd;
     111             : };
     112             : 
     113         258 : static int widelinks_connect(struct vfs_handle_struct *handle,
     114             :                         const char *service,
     115             :                         const char *user)
     116             : {
     117             :         struct widelinks_config *config;
     118             :         int ret;
     119             : 
     120         258 :         ret = SMB_VFS_NEXT_CONNECT(handle,
     121             :                                 service,
     122             :                                 user);
     123         258 :         if (ret != 0) {
     124           0 :                 return ret;
     125             :         }
     126             : 
     127         258 :         config = talloc_zero(handle->conn,
     128             :                                 struct widelinks_config);
     129         258 :         if (!config) {
     130           0 :                 SMB_VFS_NEXT_DISCONNECT(handle);
     131           0 :                 return -1;
     132             :         }
     133         258 :         config->active = lp_widelinks(SNUM(handle->conn));
     134         258 :         if (!config->active) {
     135           0 :                 DBG_ERR("vfs_widelinks module loaded with "
     136             :                         "widelinks = no\n");
     137             :         }
     138         258 :         config->is_dfs_share =
     139         258 :                 (lp_host_msdfs() && lp_msdfs_root(SNUM(handle->conn)));
     140         258 :         SMB_VFS_HANDLE_SET_DATA(handle,
     141             :                                 config,
     142             :                                 NULL, /* free_fn */
     143             :                                 struct widelinks_config,
     144             :                                 return -1);
     145         258 :         return 0;
     146             : }
     147             : 
     148        1822 : static int widelinks_chdir(struct vfs_handle_struct *handle,
     149             :                                 const struct smb_filename *smb_fname)
     150             : {
     151        1822 :         int ret = -1;
     152        1822 :         struct widelinks_config *config = NULL;
     153        1822 :         char *new_cwd = NULL;
     154             : 
     155        1822 :         SMB_VFS_HANDLE_GET_DATA(handle,
     156             :                                 config,
     157             :                                 struct widelinks_config,
     158             :                                 return -1);
     159             : 
     160        1822 :         if (!config->active) {
     161             :                 /* Module not active. */
     162           0 :                 return SMB_VFS_NEXT_CHDIR(handle, smb_fname);
     163             :         }
     164             : 
     165             :         /*
     166             :          * We know we never get a path containing
     167             :          * DOT or DOTDOT.
     168             :          */
     169             : 
     170        1822 :         if (smb_fname->base_name[0] == '/') {
     171             :                 /* Absolute path - replace. */
     172        1768 :                 new_cwd = talloc_strdup(config,
     173        1768 :                                 smb_fname->base_name);
     174             :         } else {
     175          54 :                 if (config->cwd == NULL) {
     176             :                         /*
     177             :                          * Relative chdir before absolute one -
     178             :                          * see note 1b above.
     179             :                          */
     180             :                         struct smb_filename *current_dir_fname =
     181           0 :                                         SMB_VFS_NEXT_GETWD(handle,
     182             :                                                         config);
     183           0 :                         if (current_dir_fname == NULL) {
     184           0 :                                 return -1;
     185             :                         }
     186             :                         /* Paranoia.. */
     187           0 :                         if (current_dir_fname->base_name[0] != '/') {
     188           0 :                                 DBG_ERR("SMB_VFS_NEXT_GETWD returned "
     189             :                                         "non-absolute path |%s|\n",
     190             :                                         current_dir_fname->base_name);
     191           0 :                                 TALLOC_FREE(current_dir_fname);
     192           0 :                                 return -1;
     193             :                         }
     194           0 :                         config->cwd = talloc_strdup(config,
     195           0 :                                         current_dir_fname->base_name);
     196           0 :                         TALLOC_FREE(current_dir_fname);
     197           0 :                         if (config->cwd == NULL) {
     198           0 :                                 return -1;
     199             :                         }
     200             :                 }
     201          54 :                 new_cwd = talloc_asprintf(config,
     202             :                                 "%s/%s",
     203             :                                 config->cwd,
     204          54 :                                 smb_fname->base_name);
     205             :         }
     206        1822 :         if (new_cwd == NULL) {
     207           0 :                 return -1;
     208             :         }
     209        1822 :         ret = SMB_VFS_NEXT_CHDIR(handle, smb_fname);
     210        1822 :         if (ret == -1) {
     211         196 :                 TALLOC_FREE(new_cwd);
     212         196 :                 return ret;
     213             :         }
     214             :         /* Replace the cache we use for realpath/getwd. */
     215        1626 :         TALLOC_FREE(config->cwd);
     216        1626 :         config->cwd = new_cwd;
     217        1626 :         DBG_DEBUG("config->cwd now |%s|\n", config->cwd);
     218        1626 :         return 0;
     219             : }
     220             : 
     221        1685 : static struct smb_filename *widelinks_getwd(vfs_handle_struct *handle,
     222             :                                 TALLOC_CTX *ctx)
     223             : {
     224        1685 :         struct widelinks_config *config = NULL;
     225             : 
     226        1685 :         SMB_VFS_HANDLE_GET_DATA(handle,
     227             :                                 config,
     228             :                                 struct widelinks_config,
     229             :                                 return NULL);
     230             : 
     231        1685 :         if (!config->active) {
     232             :                 /* Module not active. */
     233           0 :                 return SMB_VFS_NEXT_GETWD(handle, ctx);
     234             :         }
     235        1685 :         if (config->cwd == NULL) {
     236             :                 /* getwd before chdir. See note 1b above. */
     237           0 :                 return SMB_VFS_NEXT_GETWD(handle, ctx);
     238             :         }
     239        1685 :         return synthetic_smb_fname(ctx,
     240        1685 :                                 config->cwd,
     241             :                                 NULL,
     242             :                                 NULL,
     243             :                                 0,
     244             :                                 0);
     245             : }
     246             : 
     247        1170 : static struct smb_filename *widelinks_realpath(vfs_handle_struct *handle,
     248             :                         TALLOC_CTX *ctx,
     249             :                         const struct smb_filename *smb_fname_in)
     250             : {
     251        1170 :         struct widelinks_config *config = NULL;
     252        1170 :         char *pathname = NULL;
     253        1170 :         char *resolved_pathname = NULL;
     254             :         struct smb_filename *smb_fname;
     255             : 
     256        1170 :         SMB_VFS_HANDLE_GET_DATA(handle,
     257             :                                 config,
     258             :                                 struct widelinks_config,
     259             :                                 return NULL);
     260             : 
     261        1170 :         if (!config->active) {
     262             :                 /* Module not active. */
     263           0 :                 return SMB_VFS_NEXT_REALPATH(handle,
     264             :                                 ctx,
     265             :                                 smb_fname_in);
     266             :         }
     267             : 
     268        1170 :         if (config->cwd == NULL) {
     269             :                 /* realpath before chdir. See note 1b above. */
     270         626 :                 return SMB_VFS_NEXT_REALPATH(handle,
     271             :                                 ctx,
     272             :                                 smb_fname_in);
     273             :         }
     274             : 
     275         544 :         if (smb_fname_in->base_name[0] == '/') {
     276             :                 /* Absolute path - process as-is. */
     277         102 :                 pathname = talloc_strdup(config,
     278         102 :                                         smb_fname_in->base_name);
     279             :         } else {
     280             :                 /* Relative path - most commonly "." */
     281         442 :                 pathname = talloc_asprintf(config,
     282             :                                 "%s/%s",
     283             :                                 config->cwd,
     284         442 :                                 smb_fname_in->base_name);
     285             :         }
     286             : 
     287         544 :         SMB_ASSERT(pathname[0] == '/');
     288             : 
     289         544 :         resolved_pathname = canonicalize_absolute_path(config, pathname);
     290         544 :         if (resolved_pathname == NULL) {
     291           0 :                 TALLOC_FREE(pathname);
     292           0 :                 return NULL;
     293             :         }
     294             : 
     295         544 :         DBG_DEBUG("realpath |%s| -> |%s| -> |%s|\n",
     296             :                         smb_fname_in->base_name,
     297             :                         pathname,
     298             :                         resolved_pathname);
     299             : 
     300         544 :         smb_fname = synthetic_smb_fname(ctx,
     301             :                                 resolved_pathname,
     302             :                                 NULL,
     303             :                                 NULL,
     304             :                                 0,
     305             :                                 0);
     306         544 :         TALLOC_FREE(pathname);
     307         544 :         TALLOC_FREE(resolved_pathname);
     308         544 :         return smb_fname;
     309             : }
     310             : 
     311        3162 : static int widelinks_lstat(vfs_handle_struct *handle,
     312             :                         struct smb_filename *smb_fname)
     313             : {
     314        3162 :         struct widelinks_config *config = NULL;
     315             : 
     316        3162 :         SMB_VFS_HANDLE_GET_DATA(handle,
     317             :                                 config,
     318             :                                 struct widelinks_config,
     319             :                                 return -1);
     320             : 
     321        3162 :         if (!config->active) {
     322             :                 /* Module not active. */
     323           0 :                 return SMB_VFS_NEXT_LSTAT(handle,
     324             :                                 smb_fname);
     325             :         }
     326             : 
     327        3162 :         if (config->cwd == NULL) {
     328             :                 /* lstat before chdir. See note 1b above. */
     329           0 :                 return SMB_VFS_NEXT_LSTAT(handle,
     330             :                                 smb_fname);
     331             :         }
     332             : 
     333        3162 :         if (smb_fname->flags & SMB_FILENAME_POSIX_PATH) {
     334             :                 /* POSIX sees symlinks. */
     335           0 :                 return SMB_VFS_NEXT_LSTAT(handle,
     336             :                                 smb_fname);
     337             :         }
     338             : 
     339             :         /* Replace with STAT. */
     340        3162 :         return SMB_VFS_NEXT_STAT(handle, smb_fname);
     341             : }
     342             : 
     343        2920 : static int widelinks_openat(vfs_handle_struct *handle,
     344             :                             const struct files_struct *dirfsp,
     345             :                             const struct smb_filename *smb_fname,
     346             :                             files_struct *fsp,
     347             :                             const struct vfs_open_how *_how)
     348             : {
     349        2920 :         struct vfs_open_how how = *_how;
     350        2920 :         struct widelinks_config *config = NULL;
     351             :         int ret;
     352        2920 :         SMB_VFS_HANDLE_GET_DATA(handle,
     353             :                                 config,
     354             :                                 struct widelinks_config,
     355             :                                 return -1);
     356             : 
     357        2920 :         if (config->active &&
     358        2920 :             (config->cwd != NULL) &&
     359        2920 :             !(smb_fname->flags & SMB_FILENAME_POSIX_PATH))
     360             :         {
     361             :                 /*
     362             :                  * Module active, openat after chdir (see note 1b above) and not
     363             :                  * a POSIX open (POSIX sees symlinks), so remove O_NOFOLLOW.
     364             :                  */
     365        2920 :                 how.flags = (how.flags & ~O_NOFOLLOW);
     366             :         }
     367             : 
     368        2920 :         ret = SMB_VFS_NEXT_OPENAT(handle,
     369             :                                    dirfsp,
     370             :                                    smb_fname,
     371             :                                    fsp,
     372             :                                    &how);
     373        2920 :         if (config->is_dfs_share && ret == -1 && errno == ENOENT) {
     374          25 :                 struct smb_filename *full_fname = NULL;
     375             :                 int lstat_ret;
     376             : 
     377          25 :                 full_fname = full_path_from_dirfsp_atname(talloc_tos(),
     378             :                                 dirfsp,
     379             :                                 smb_fname);
     380          25 :                 if (full_fname == NULL) {
     381           0 :                         errno = ENOMEM;
     382           0 :                         return -1;
     383             :                 }
     384          25 :                 lstat_ret = SMB_VFS_NEXT_LSTAT(handle,
     385             :                                 full_fname);
     386          25 :                 if (lstat_ret != -1 &&
     387           4 :                     VALID_STAT(full_fname->st) &&
     388           4 :                     S_ISLNK(full_fname->st.st_ex_mode)) {
     389           4 :                         fsp->fsp_name->st = full_fname->st;
     390             :                 }
     391          25 :                 TALLOC_FREE(full_fname);
     392          25 :                 errno = ELOOP;
     393             :         }
     394        2920 :         return ret;
     395             : }
     396             : 
     397             : static struct vfs_fn_pointers vfs_widelinks_fns = {
     398             :         .connect_fn = widelinks_connect,
     399             : 
     400             :         .openat_fn = widelinks_openat,
     401             :         .lstat_fn = widelinks_lstat,
     402             :         /*
     403             :          * NB. We don't need an lchown function as this
     404             :          * is only called (a) on directory create and
     405             :          * (b) on POSIX extensions names.
     406             :          */
     407             :         .chdir_fn = widelinks_chdir,
     408             :         .getwd_fn = widelinks_getwd,
     409             :         .realpath_fn = widelinks_realpath,
     410             : };
     411             : 
     412             : static_decl_vfs;
     413         273 : NTSTATUS vfs_widelinks_init(TALLOC_CTX *ctx)
     414             : {
     415         273 :         return smb_register_vfs(SMB_VFS_INTERFACE_VERSION,
     416             :                                 "widelinks",
     417             :                                 &vfs_widelinks_fns);
     418             : }

Generated by: LCOV version 1.14