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 : char *cwd;
110 : };
111 :
112 208 : static int widelinks_connect(struct vfs_handle_struct *handle,
113 : const char *service,
114 : const char *user)
115 : {
116 : struct widelinks_config *config;
117 : int ret;
118 :
119 208 : ret = SMB_VFS_NEXT_CONNECT(handle,
120 : service,
121 : user);
122 208 : if (ret != 0) {
123 0 : return ret;
124 : }
125 :
126 208 : config = talloc_zero(handle->conn,
127 : struct widelinks_config);
128 208 : if (!config) {
129 0 : SMB_VFS_NEXT_DISCONNECT(handle);
130 0 : return -1;
131 : }
132 208 : config->active = lp_widelinks(SNUM(handle->conn));
133 208 : if (!config->active) {
134 0 : DBG_ERR("vfs_widelinks module loaded with "
135 : "widelinks = no\n");
136 : }
137 :
138 208 : SMB_VFS_HANDLE_SET_DATA(handle,
139 : config,
140 : NULL, /* free_fn */
141 : struct widelinks_config,
142 : return -1);
143 208 : return 0;
144 : }
145 :
146 2714 : static int widelinks_chdir(struct vfs_handle_struct *handle,
147 : const struct smb_filename *smb_fname)
148 : {
149 2714 : int ret = -1;
150 2714 : struct widelinks_config *config = NULL;
151 2714 : char *new_cwd = NULL;
152 :
153 2714 : SMB_VFS_HANDLE_GET_DATA(handle,
154 : config,
155 : struct widelinks_config,
156 : return -1);
157 :
158 2714 : if (!config->active) {
159 : /* Module not active. */
160 0 : return SMB_VFS_NEXT_CHDIR(handle, smb_fname);
161 : }
162 :
163 : /*
164 : * We know we never get a path containing
165 : * DOT or DOTDOT.
166 : */
167 :
168 2714 : if (smb_fname->base_name[0] == '/') {
169 : /* Absolute path - replace. */
170 2194 : new_cwd = talloc_strdup(config,
171 2194 : smb_fname->base_name);
172 : } else {
173 520 : if (config->cwd == NULL) {
174 : /*
175 : * Relative chdir before absolute one -
176 : * see note 1b above.
177 : */
178 0 : struct smb_filename *current_dir_fname =
179 0 : SMB_VFS_NEXT_GETWD(handle,
180 : config);
181 0 : if (current_dir_fname == NULL) {
182 0 : return -1;
183 : }
184 : /* Paranoia.. */
185 0 : if (current_dir_fname->base_name[0] != '/') {
186 0 : DBG_ERR("SMB_VFS_NEXT_GETWD returned "
187 : "non-absolute path |%s|\n",
188 : current_dir_fname->base_name);
189 0 : TALLOC_FREE(current_dir_fname);
190 0 : return -1;
191 : }
192 0 : config->cwd = talloc_strdup(config,
193 0 : current_dir_fname->base_name);
194 0 : TALLOC_FREE(current_dir_fname);
195 0 : if (config->cwd == NULL) {
196 0 : return -1;
197 : }
198 : }
199 520 : new_cwd = talloc_asprintf(config,
200 : "%s/%s",
201 : config->cwd,
202 0 : smb_fname->base_name);
203 : }
204 2714 : if (new_cwd == NULL) {
205 0 : return -1;
206 : }
207 2714 : ret = SMB_VFS_NEXT_CHDIR(handle, smb_fname);
208 2714 : if (ret == -1) {
209 198 : TALLOC_FREE(new_cwd);
210 198 : return ret;
211 : }
212 : /* Replace the cache we use for realpath/getwd. */
213 2516 : TALLOC_FREE(config->cwd);
214 2516 : config->cwd = new_cwd;
215 2516 : DBG_DEBUG("config->cwd now |%s|\n", config->cwd);
216 2516 : return 0;
217 : }
218 :
219 3064 : static struct smb_filename *widelinks_getwd(vfs_handle_struct *handle,
220 : TALLOC_CTX *ctx)
221 : {
222 3064 : struct widelinks_config *config = NULL;
223 :
224 3064 : SMB_VFS_HANDLE_GET_DATA(handle,
225 : config,
226 : struct widelinks_config,
227 : return NULL);
228 :
229 3064 : if (!config->active) {
230 : /* Module not active. */
231 0 : return SMB_VFS_NEXT_GETWD(handle, ctx);
232 : }
233 3064 : if (config->cwd == NULL) {
234 : /* getwd before chdir. See note 1b above. */
235 0 : return SMB_VFS_NEXT_GETWD(handle, ctx);
236 : }
237 3064 : return synthetic_smb_fname(ctx,
238 3064 : config->cwd,
239 : NULL,
240 : NULL,
241 : 0,
242 : 0);
243 : }
244 :
245 1988 : static struct smb_filename *widelinks_realpath(vfs_handle_struct *handle,
246 : TALLOC_CTX *ctx,
247 : const struct smb_filename *smb_fname_in)
248 : {
249 1988 : struct widelinks_config *config = NULL;
250 1988 : char *pathname = NULL;
251 1988 : char *resolved_pathname = NULL;
252 : struct smb_filename *smb_fname;
253 :
254 1988 : SMB_VFS_HANDLE_GET_DATA(handle,
255 : config,
256 : struct widelinks_config,
257 : return NULL);
258 :
259 1988 : if (!config->active) {
260 : /* Module not active. */
261 0 : return SMB_VFS_NEXT_REALPATH(handle,
262 : ctx,
263 : smb_fname_in);
264 : }
265 :
266 1988 : if (config->cwd == NULL) {
267 : /* realpath before chdir. See note 1b above. */
268 416 : return SMB_VFS_NEXT_REALPATH(handle,
269 : ctx,
270 : smb_fname_in);
271 : }
272 :
273 1572 : if (smb_fname_in->base_name[0] == '/') {
274 : /* Absolute path - process as-is. */
275 312 : pathname = talloc_strdup(config,
276 312 : smb_fname_in->base_name);
277 : } else {
278 : /* Relative path - most commonly "." */
279 1260 : pathname = talloc_asprintf(config,
280 : "%s/%s",
281 : config->cwd,
282 0 : smb_fname_in->base_name);
283 : }
284 :
285 1572 : SMB_ASSERT(pathname[0] == '/');
286 :
287 1572 : resolved_pathname = canonicalize_absolute_path(config, pathname);
288 1572 : if (resolved_pathname == NULL) {
289 0 : TALLOC_FREE(pathname);
290 0 : return NULL;
291 : }
292 :
293 1572 : DBG_DEBUG("realpath |%s| -> |%s| -> |%s|\n",
294 : smb_fname_in->base_name,
295 : pathname,
296 : resolved_pathname);
297 :
298 1572 : smb_fname = synthetic_smb_fname(ctx,
299 : resolved_pathname,
300 : NULL,
301 : NULL,
302 : 0,
303 : 0);
304 1572 : TALLOC_FREE(pathname);
305 1572 : TALLOC_FREE(resolved_pathname);
306 1572 : return smb_fname;
307 : }
308 :
309 3510 : static int widelinks_lstat(vfs_handle_struct *handle,
310 : struct smb_filename *smb_fname)
311 : {
312 3510 : struct widelinks_config *config = NULL;
313 :
314 3510 : SMB_VFS_HANDLE_GET_DATA(handle,
315 : config,
316 : struct widelinks_config,
317 : return -1);
318 :
319 3510 : if (!config->active) {
320 : /* Module not active. */
321 0 : return SMB_VFS_NEXT_LSTAT(handle,
322 : smb_fname);
323 : }
324 :
325 3510 : if (config->cwd == NULL) {
326 : /* lstat before chdir. See note 1b above. */
327 0 : return SMB_VFS_NEXT_LSTAT(handle,
328 : smb_fname);
329 : }
330 :
331 3510 : if (smb_fname->flags & SMB_FILENAME_POSIX_PATH) {
332 : /* POSIX sees symlinks. */
333 0 : return SMB_VFS_NEXT_LSTAT(handle,
334 : smb_fname);
335 : }
336 :
337 : /* Replace with STAT. */
338 3510 : return SMB_VFS_NEXT_STAT(handle, smb_fname);
339 : }
340 :
341 1848 : static int widelinks_openat(vfs_handle_struct *handle,
342 : const struct files_struct *dirfsp,
343 : const struct smb_filename *smb_fname,
344 : files_struct *fsp,
345 : int flags,
346 : mode_t mode)
347 : {
348 1848 : struct widelinks_config *config = NULL;
349 :
350 1848 : SMB_VFS_HANDLE_GET_DATA(handle,
351 : config,
352 : struct widelinks_config,
353 : return -1);
354 :
355 3696 : if (config->active &&
356 3696 : (config->cwd != NULL) &&
357 1848 : !(smb_fname->flags & SMB_FILENAME_POSIX_PATH))
358 : {
359 : /*
360 : * Module active, openat after chdir (see note 1b above) and not
361 : * a POSIX open (POSIX sees symlinks), so remove O_NOFOLLOW.
362 : */
363 1848 : flags = (flags & ~O_NOFOLLOW);
364 : }
365 :
366 1848 : return SMB_VFS_NEXT_OPENAT(handle,
367 : dirfsp,
368 : smb_fname,
369 : fsp,
370 : flags,
371 : mode);
372 : }
373 :
374 1236 : static struct dirent *widelinks_readdir(vfs_handle_struct *handle,
375 : struct files_struct *dirfsp,
376 : DIR *dirp,
377 : SMB_STRUCT_STAT *sbuf)
378 : {
379 1236 : struct widelinks_config *config = NULL;
380 : struct dirent *result;
381 :
382 1236 : SMB_VFS_HANDLE_GET_DATA(handle,
383 : config,
384 : struct widelinks_config,
385 : return NULL);
386 :
387 1236 : result = SMB_VFS_NEXT_READDIR(handle,
388 : dirfsp,
389 : dirp,
390 : sbuf);
391 :
392 1236 : if (!config->active) {
393 : /* Module not active. */
394 0 : return result;
395 : }
396 :
397 : /*
398 : * Prevent optimization of returning
399 : * the stat info. Force caller to go
400 : * through our LSTAT that hides symlinks.
401 : */
402 :
403 1236 : if (sbuf) {
404 84 : SET_STAT_INVALID(*sbuf);
405 : }
406 1236 : return result;
407 : }
408 :
409 : static struct vfs_fn_pointers vfs_widelinks_fns = {
410 : .connect_fn = widelinks_connect,
411 :
412 : .openat_fn = widelinks_openat,
413 : .lstat_fn = widelinks_lstat,
414 : /*
415 : * NB. We don't need an lchown function as this
416 : * is only called (a) on directory create and
417 : * (b) on POSIX extensions names.
418 : */
419 : .chdir_fn = widelinks_chdir,
420 : .getwd_fn = widelinks_getwd,
421 : .realpath_fn = widelinks_realpath,
422 : .readdir_fn = widelinks_readdir
423 : };
424 :
425 : static_decl_vfs;
426 222 : NTSTATUS vfs_widelinks_init(TALLOC_CTX *ctx)
427 : {
428 222 : return smb_register_vfs(SMB_VFS_INTERFACE_VERSION,
429 : "widelinks",
430 : &vfs_widelinks_fns);
431 : }
|