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 existance 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 8 : 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 8 : ret = SMB_VFS_NEXT_CONNECT(handle,
121 : service,
122 : user);
123 8 : if (ret != 0) {
124 0 : return ret;
125 : }
126 :
127 8 : config = talloc_zero(handle->conn,
128 : struct widelinks_config);
129 8 : if (!config) {
130 0 : SMB_VFS_NEXT_DISCONNECT(handle);
131 0 : return -1;
132 : }
133 8 : config->active = lp_widelinks(SNUM(handle->conn));
134 8 : if (!config->active) {
135 0 : DBG_ERR("vfs_widelinks module loaded with "
136 : "widelinks = no\n");
137 : }
138 8 : config->is_dfs_share =
139 8 : (lp_host_msdfs() && lp_msdfs_root(SNUM(handle->conn)));
140 8 : SMB_VFS_HANDLE_SET_DATA(handle,
141 : config,
142 : NULL, /* free_fn */
143 : struct widelinks_config,
144 : return -1);
145 8 : return 0;
146 : }
147 :
148 56 : static int widelinks_chdir(struct vfs_handle_struct *handle,
149 : const struct smb_filename *smb_fname)
150 : {
151 56 : int ret = -1;
152 56 : struct widelinks_config *config = NULL;
153 56 : char *new_cwd = NULL;
154 :
155 56 : SMB_VFS_HANDLE_GET_DATA(handle,
156 : config,
157 : struct widelinks_config,
158 : return -1);
159 :
160 56 : 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 56 : if (smb_fname->base_name[0] == '/') {
171 : /* Absolute path - replace. */
172 48 : new_cwd = talloc_strdup(config,
173 48 : smb_fname->base_name);
174 : } else {
175 8 : if (config->cwd == NULL) {
176 : /*
177 : * Relative chdir before absolute one -
178 : * see note 1b above.
179 : */
180 0 : 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 8 : new_cwd = talloc_asprintf(config,
202 : "%s/%s",
203 : config->cwd,
204 4 : smb_fname->base_name);
205 : }
206 56 : if (new_cwd == NULL) {
207 0 : return -1;
208 : }
209 56 : ret = SMB_VFS_NEXT_CHDIR(handle, smb_fname);
210 56 : if (ret == -1) {
211 40 : TALLOC_FREE(new_cwd);
212 40 : return ret;
213 : }
214 : /* Replace the cache we use for realpath/getwd. */
215 16 : TALLOC_FREE(config->cwd);
216 16 : config->cwd = new_cwd;
217 16 : DBG_DEBUG("config->cwd now |%s|\n", config->cwd);
218 16 : return 0;
219 : }
220 :
221 0 : static struct smb_filename *widelinks_getwd(vfs_handle_struct *handle,
222 : TALLOC_CTX *ctx)
223 : {
224 0 : struct widelinks_config *config = NULL;
225 :
226 0 : SMB_VFS_HANDLE_GET_DATA(handle,
227 : config,
228 : struct widelinks_config,
229 : return NULL);
230 :
231 0 : if (!config->active) {
232 : /* Module not active. */
233 0 : return SMB_VFS_NEXT_GETWD(handle, ctx);
234 : }
235 0 : if (config->cwd == NULL) {
236 : /* getwd before chdir. See note 1b above. */
237 0 : return SMB_VFS_NEXT_GETWD(handle, ctx);
238 : }
239 0 : return synthetic_smb_fname(ctx,
240 0 : config->cwd,
241 : NULL,
242 : NULL,
243 : 0,
244 : 0);
245 : }
246 :
247 24 : static struct smb_filename *widelinks_realpath(vfs_handle_struct *handle,
248 : TALLOC_CTX *ctx,
249 : const struct smb_filename *smb_fname_in)
250 : {
251 24 : struct widelinks_config *config = NULL;
252 24 : char *pathname = NULL;
253 24 : char *resolved_pathname = NULL;
254 : struct smb_filename *smb_fname;
255 :
256 24 : SMB_VFS_HANDLE_GET_DATA(handle,
257 : config,
258 : struct widelinks_config,
259 : return NULL);
260 :
261 24 : if (!config->active) {
262 : /* Module not active. */
263 0 : return SMB_VFS_NEXT_REALPATH(handle,
264 : ctx,
265 : smb_fname_in);
266 : }
267 :
268 24 : if (config->cwd == NULL) {
269 : /* realpath before chdir. See note 1b above. */
270 16 : return SMB_VFS_NEXT_REALPATH(handle,
271 : ctx,
272 : smb_fname_in);
273 : }
274 :
275 8 : if (smb_fname_in->base_name[0] == '/') {
276 : /* Absolute path - process as-is. */
277 0 : pathname = talloc_strdup(config,
278 0 : smb_fname_in->base_name);
279 : } else {
280 : /* Relative path - most commonly "." */
281 8 : pathname = talloc_asprintf(config,
282 : "%s/%s",
283 : config->cwd,
284 4 : smb_fname_in->base_name);
285 : }
286 :
287 8 : SMB_ASSERT(pathname[0] == '/');
288 :
289 8 : resolved_pathname = canonicalize_absolute_path(config, pathname);
290 8 : if (resolved_pathname == NULL) {
291 0 : TALLOC_FREE(pathname);
292 0 : return NULL;
293 : }
294 :
295 8 : DBG_DEBUG("realpath |%s| -> |%s| -> |%s|\n",
296 : smb_fname_in->base_name,
297 : pathname,
298 : resolved_pathname);
299 :
300 8 : smb_fname = synthetic_smb_fname(ctx,
301 : resolved_pathname,
302 : NULL,
303 : NULL,
304 : 0,
305 : 0);
306 8 : TALLOC_FREE(pathname);
307 8 : TALLOC_FREE(resolved_pathname);
308 8 : return smb_fname;
309 : }
310 :
311 0 : static int widelinks_lstat(vfs_handle_struct *handle,
312 : struct smb_filename *smb_fname)
313 : {
314 0 : struct widelinks_config *config = NULL;
315 :
316 0 : SMB_VFS_HANDLE_GET_DATA(handle,
317 : config,
318 : struct widelinks_config,
319 : return -1);
320 :
321 0 : if (!config->active) {
322 : /* Module not active. */
323 0 : return SMB_VFS_NEXT_LSTAT(handle,
324 : smb_fname);
325 : }
326 :
327 0 : 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 0 : 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 0 : return SMB_VFS_NEXT_STAT(handle, smb_fname);
341 : }
342 :
343 16 : 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 16 : struct vfs_open_how how = *_how;
350 16 : struct widelinks_config *config = NULL;
351 : int ret;
352 16 : SMB_VFS_HANDLE_GET_DATA(handle,
353 : config,
354 : struct widelinks_config,
355 : return -1);
356 :
357 24 : if (config->active &&
358 16 : (config->cwd != NULL) &&
359 0 : !(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 0 : how.flags = (how.flags & ~O_NOFOLLOW);
366 : }
367 :
368 16 : ret = SMB_VFS_NEXT_OPENAT(handle,
369 : dirfsp,
370 : smb_fname,
371 : fsp,
372 : &how);
373 16 : if (config->is_dfs_share && ret == -1 && errno == ENOENT) {
374 0 : struct smb_filename *full_fname = NULL;
375 : int lstat_ret;
376 :
377 0 : full_fname = full_path_from_dirfsp_atname(talloc_tos(),
378 : dirfsp,
379 : smb_fname);
380 0 : if (full_fname == NULL) {
381 0 : errno = ENOMEM;
382 0 : return -1;
383 : }
384 0 : lstat_ret = SMB_VFS_NEXT_LSTAT(handle,
385 : full_fname);
386 0 : if (lstat_ret != -1 &&
387 0 : VALID_STAT(full_fname->st) &&
388 0 : S_ISLNK(full_fname->st.st_ex_mode)) {
389 0 : fsp->fsp_name->st = full_fname->st;
390 : }
391 0 : TALLOC_FREE(full_fname);
392 0 : errno = ENOENT;
393 : }
394 16 : return ret;
395 : }
396 :
397 0 : static struct dirent *widelinks_readdir(vfs_handle_struct *handle,
398 : struct files_struct *dirfsp,
399 : DIR *dirp,
400 : SMB_STRUCT_STAT *sbuf)
401 : {
402 0 : struct widelinks_config *config = NULL;
403 : struct dirent *result;
404 :
405 0 : SMB_VFS_HANDLE_GET_DATA(handle,
406 : config,
407 : struct widelinks_config,
408 : return NULL);
409 :
410 0 : result = SMB_VFS_NEXT_READDIR(handle,
411 : dirfsp,
412 : dirp,
413 : sbuf);
414 :
415 0 : if (!config->active) {
416 : /* Module not active. */
417 0 : return result;
418 : }
419 :
420 : /*
421 : * Prevent optimization of returning
422 : * the stat info. Force caller to go
423 : * through our LSTAT that hides symlinks.
424 : */
425 :
426 0 : if (sbuf) {
427 0 : SET_STAT_INVALID(*sbuf);
428 : }
429 0 : return result;
430 : }
431 :
432 : static struct vfs_fn_pointers vfs_widelinks_fns = {
433 : .connect_fn = widelinks_connect,
434 :
435 : .openat_fn = widelinks_openat,
436 : .lstat_fn = widelinks_lstat,
437 : /*
438 : * NB. We don't need an lchown function as this
439 : * is only called (a) on directory create and
440 : * (b) on POSIX extensions names.
441 : */
442 : .chdir_fn = widelinks_chdir,
443 : .getwd_fn = widelinks_getwd,
444 : .realpath_fn = widelinks_realpath,
445 : .readdir_fn = widelinks_readdir
446 : };
447 :
448 : static_decl_vfs;
449 34 : NTSTATUS vfs_widelinks_init(TALLOC_CTX *ctx)
450 : {
451 34 : return smb_register_vfs(SMB_VFS_INTERFACE_VERSION,
452 : "widelinks",
453 : &vfs_widelinks_fns);
454 : }
|