1 /*
2  * ProFTPD: mod_vroot -- a module implementing a virtual chroot capability
3  *                       via the FSIO API
4  * Copyright (c) 2002-2016 TJ Saunders
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 2 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, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
19  *
20  * As a special exemption, TJ Saunders and other respective copyright holders
21  * give permission to link this program with OpenSSL, and distribute the
22  * resulting executable, without including the source code for OpenSSL in the
23  * source distribution.
24  *
25  * This is mod_vroot, contrib software for proftpd 1.3.x and above.
26  * For more information contact TJ Saunders <tj@castaglia.org>.
27  *
28  * -----DO NOT EDIT BELOW THIS LINE-----
29  * $Archive: mod_vroot.a $
30  */
31 
32 #include "mod_vroot.h"
33 #include "privs.h"
34 #include "alias.h"
35 #include "path.h"
36 #include "fsio.h"
37 
38 int vroot_logfd = -1;
39 unsigned int vroot_opts = 0;
40 
41 module vroot_module;
42 
43 static int vroot_engine = FALSE;
44 
45 #if PROFTPD_VERSION_NUMBER >= 0x0001030407
46 static int vroot_use_mkdtemp = FALSE;
47 #endif /* ProFTPD 1.3.4c or later */
48 
handle_vrootaliases(void)49 static int handle_vrootaliases(void) {
50   config_rec *c;
51   pool *tmp_pool = NULL;
52 
53   /* Handle any VRootAlias settings. */
54 
55   tmp_pool = make_sub_pool(session.pool);
56   pr_pool_tag(tmp_pool, "VRootAlias pool");
57 
58   c = find_config(main_server->conf, CONF_PARAM, "VRootAlias", FALSE);
59   while (c != NULL) {
60     char src_path[PR_TUNABLE_PATH_MAX+1], dst_path[PR_TUNABLE_PATH_MAX+1];
61     const char *ptr;
62 
63     pr_signals_handle();
64 
65     /* XXX Note that by using vroot_path_lookup(), we assume a POST_CMD
66      * invocation.  Looks like VRootAlias might end up being incompatible
67      * with VRootServerRoot.
68      */
69 
70     memset(src_path, '\0', sizeof(src_path));
71     ptr = c->argv[0];
72 
73     /* Check for any expandable variables. */
74     ptr = path_subst_uservar(tmp_pool, &ptr);
75 
76     sstrncpy(src_path, ptr, sizeof(src_path)-1);
77     vroot_path_clean(src_path);
78 
79     ptr = c->argv[1];
80 
81     /* Check for any expandable variables. */
82     ptr = path_subst_uservar(tmp_pool, &ptr);
83 
84     ptr = dir_best_path(tmp_pool, ptr);
85     vroot_path_lookup(NULL, dst_path, sizeof(dst_path)-1, ptr,
86       VROOT_LOOKUP_FL_NO_ALIAS, NULL);
87 
88     if (vroot_alias_add(dst_path, src_path) < 0) {
89       /* Make a slightly better log message when there is an alias collision. */
90       if (errno == EEXIST) {
91         (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION,
92           "VRootAlias already configured for '%s', ignoring bad alias",
93           (char *) c->argv[1]);
94 
95       } else {
96         (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION,
97           "error stashing VRootAlias '%s': %s", dst_path, strerror(errno));
98       }
99 
100     } else {
101       (void) pr_log_writefile(vroot_logfd, MOD_VROOT_VERSION,
102         "aliased '%s' to real path '%s'", dst_path, src_path);
103     }
104 
105     c = find_config_next(c, c->next, CONF_PARAM, "VRootAlias", FALSE);
106   }
107 
108   destroy_pool(tmp_pool);
109   return 0;
110 }
111 
112 /* Configuration handlers
113  */
114 
115 /* usage: VRootAlias src-path dst-path */
set_vrootalias(cmd_rec * cmd)116 MODRET set_vrootalias(cmd_rec *cmd) {
117   config_rec *c;
118 
119   CHECK_ARGS(cmd, 2);
120   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
121 
122   if (pr_fs_valid_path(cmd->argv[1]) < 0) {
123     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "source path '", cmd->argv[1],
124       "' is not an absolute path", NULL));
125   }
126 
127   c = add_config_param_str(cmd->argv[0], 2, cmd->argv[1], cmd->argv[2]);
128 
129   /* Set this flag in order to allow mod_ifsession to work properly with
130    * multiple VRootAlias directives.
131    */
132   c->flags |= CF_MERGEDOWN_MULTI;
133 
134   return PR_HANDLED(cmd);
135 }
136 
137 /* usage: VRootEngine on|off */
set_vrootengine(cmd_rec * cmd)138 MODRET set_vrootengine(cmd_rec *cmd) {
139   int engine = -1;
140   config_rec *c = NULL;
141 
142   CHECK_ARGS(cmd, 1);
143   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
144 
145   engine = get_boolean(cmd, 1);
146   if (engine == -1) {
147     CONF_ERROR(cmd, "expected Boolean parameter");
148   }
149 
150   c = add_config_param(cmd->argv[0], 1, NULL);
151   c->argv[0] = pcalloc(c->pool, sizeof(int));
152   *((int *) c->argv[0]) = engine;
153 
154   return PR_HANDLED(cmd);
155 }
156 
157 /* usage: VRootLog path|"none" */
set_vrootlog(cmd_rec * cmd)158 MODRET set_vrootlog(cmd_rec *cmd) {
159   CHECK_ARGS(cmd, 1);
160   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
161 
162   if (pr_fs_valid_path(cmd->argv[1]) < 0) {
163     CONF_ERROR(cmd, "must be an absolute path");
164   }
165 
166   (void) add_config_param_str(cmd->argv[0], 1, cmd->argv[1]);
167   return PR_HANDLED(cmd);
168 }
169 
170 /* usage: VRootOptions opt1 opt2 ... optN */
set_vrootoptions(cmd_rec * cmd)171 MODRET set_vrootoptions(cmd_rec *cmd) {
172   config_rec *c = NULL;
173   register unsigned int i;
174   unsigned int opts = 0U;
175 
176   if (cmd->argc-1 == 0) {
177     CONF_ERROR(cmd, "wrong number of parameters");
178   }
179 
180   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
181 
182   c = add_config_param(cmd->argv[0], 1, NULL);
183   for (i = 1; i < cmd->argc; i++) {
184     if (strcasecmp(cmd->argv[i], "AllowSymlinks") == 0) {
185       opts |= VROOT_OPT_ALLOW_SYMLINKS;
186 
187     } else {
188       CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, ": unknown VRootOption: '",
189         cmd->argv[i], "'", NULL));
190     }
191   }
192 
193   c->argv[0] = pcalloc(c->pool, sizeof(unsigned int));
194   *((unsigned int *) c->argv[0]) = opts;
195 
196   return PR_HANDLED(cmd);
197 }
198 
199 /* usage: VRootServerRoot path */
set_vrootserverroot(cmd_rec * cmd)200 MODRET set_vrootserverroot(cmd_rec *cmd) {
201   struct stat st;
202   config_rec *c;
203   char *path;
204   size_t pathlen;
205 
206   CHECK_ARGS(cmd, 1);
207   CHECK_CONF(cmd, CONF_ROOT|CONF_VIRTUAL|CONF_GLOBAL);
208 
209   path = cmd->argv[1];
210 
211   if (pr_fs_valid_path(path) < 0) {
212     CONF_ERROR(cmd, "must be an absolute path");
213   }
214 
215   if (stat(path, &st) < 0) {
216     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "error checking '", path, "': ",
217       strerror(errno), NULL));
218   }
219 
220   if (!S_ISDIR(st.st_mode)) {
221     CONF_ERROR(cmd, pstrcat(cmd->tmp_pool, "'", path, "' is not a directory",
222       NULL));
223   }
224 
225   c = add_config_param(cmd->argv[0], 1, NULL);
226 
227   /* Make sure the configured path has a trailing path separater ('/').
228    * This is important.
229    */
230 
231   pathlen = strlen(path);
232   if (path[pathlen - 1] != '/') {
233     c->argv[0] = pstrcat(c->pool, path, "/", NULL);
234 
235   } else {
236     c->argv[0] = pstrdup(c->pool, path);
237   }
238 
239   return PR_HANDLED(cmd);
240 }
241 
242 /* Command handlers
243  */
244 
vroot_log_retr(cmd_rec * cmd)245 MODRET vroot_log_retr(cmd_rec *cmd) {
246   const char *key, *path;
247 
248   if (vroot_engine == FALSE ||
249       session.chroot_path == NULL) {
250     return PR_DECLINED(cmd);
251   }
252 
253   key = "mod_xfer.retr-path";
254 
255   path = pr_table_get(cmd->notes, key, NULL);
256   if (path != NULL) {
257     char *real_path;
258 
259     if (*path == '/') {
260       const char *base_path;
261 
262       base_path = vroot_path_get_base(cmd->tmp_pool, NULL);
263       real_path = pdircat(cmd->pool, base_path, path, NULL);
264       vroot_path_clean(real_path);
265 
266     } else {
267       real_path = vroot_realpath(cmd->pool, path, VROOT_REALPATH_FL_ABS_PATH);
268     }
269 
270     pr_table_set(cmd->notes, key, real_path, 0);
271   }
272 
273   return PR_DECLINED(cmd);
274 }
275 
vroot_log_stor(cmd_rec * cmd)276 MODRET vroot_log_stor(cmd_rec *cmd) {
277   const char *key, *path;
278 
279   if (vroot_engine == FALSE ||
280       session.chroot_path == NULL) {
281     return PR_DECLINED(cmd);
282   }
283 
284   key = "mod_xfer.store-path";
285 
286   path = pr_table_get(cmd->notes, key, NULL);
287   if (path != NULL) {
288     char *real_path;
289 
290     if (*path == '/') {
291       const char *base_path;
292 
293       base_path = vroot_path_get_base(cmd->tmp_pool, NULL);
294       real_path = pdircat(cmd->pool, base_path, path, NULL);
295       vroot_path_clean(real_path);
296 
297     } else {
298       real_path = vroot_realpath(cmd->pool, path, VROOT_REALPATH_FL_ABS_PATH);
299     }
300 
301     pr_table_set(cmd->notes, key, real_path, 0);
302   }
303 
304   return PR_DECLINED(cmd);
305 }
306 
vroot_pre_mkd(cmd_rec * cmd)307 MODRET vroot_pre_mkd(cmd_rec *cmd) {
308   if (vroot_engine == FALSE ||
309       session.chroot_path == NULL) {
310     return PR_DECLINED(cmd);
311   }
312 
313 #if PROFTPD_VERSION_NUMBER >= 0x0001030407
314   vroot_use_mkdtemp = pr_fsio_set_use_mkdtemp(FALSE);
315 #endif /* ProFTPD 1.3.4c or later */
316 
317   return PR_DECLINED(cmd);
318 }
319 
vroot_post_mkd(cmd_rec * cmd)320 MODRET vroot_post_mkd(cmd_rec *cmd) {
321   if (vroot_engine == FALSE ||
322       session.chroot_path == NULL) {
323     return PR_DECLINED(cmd);
324   }
325 
326 #if PROFTPD_VERSION_NUMBER >= 0x0001030407
327   pr_fsio_set_use_mkdtemp(vroot_use_mkdtemp);
328 #endif /* ProFTPD 1.3.4c or later */
329 
330   return PR_DECLINED(cmd);
331 }
332 
vroot_pre_pass(cmd_rec * cmd)333 MODRET vroot_pre_pass(cmd_rec *cmd) {
334   pr_fs_t *fs = NULL;
335   int *use_vroot = NULL;
336 
337   use_vroot = get_param_ptr(main_server->conf, "VRootEngine", FALSE);
338   if (use_vroot == NULL ||
339       *use_vroot == FALSE) {
340     vroot_engine = FALSE;
341     return PR_DECLINED(cmd);
342   }
343 
344   /* First, make sure that we have not already registered our FS object. */
345   fs = pr_unmount_fs("/", "vroot");
346   if (fs != NULL) {
347     destroy_pool(fs->fs_pool);
348   }
349 
350   fs = pr_register_fs(main_server->pool, "vroot", "/");
351   if (fs == NULL) {
352     pr_log_debug(DEBUG3, MOD_VROOT_VERSION ": error registering fs: %s",
353       strerror(errno));
354     return PR_DECLINED(cmd);
355   }
356 
357   pr_log_debug(DEBUG5, MOD_VROOT_VERSION ": vroot registered");
358 
359   /* Add the module's custom FS callbacks here. This module does not
360    * provide callbacks for the following (as they are unnecessary):
361    * close(), read(), write(), and lseek().
362    */
363   fs->stat = vroot_fsio_stat;
364   fs->lstat = vroot_fsio_lstat;
365   fs->rename = vroot_fsio_rename;
366   fs->unlink = vroot_fsio_unlink;
367   fs->open = vroot_fsio_open;
368 #if PROFTPD_VERSION_NUMBER < 0x0001030603
369   fs->creat = vroot_fsio_creat;
370 #endif /* ProFTPD 1.3.6rc2 or earlier */
371   fs->link = vroot_fsio_link;
372   fs->readlink = vroot_fsio_readlink;
373   fs->symlink = vroot_fsio_symlink;
374   fs->truncate = vroot_fsio_truncate;
375   fs->chmod = vroot_fsio_chmod;
376   fs->chown = vroot_fsio_chown;
377 #if PROFTPD_VERSION_NUMBER >= 0x0001030407
378   fs->lchown = vroot_fsio_lchown;
379 #endif /* ProFTPD 1.3.4c or later */
380   fs->chdir = vroot_fsio_chdir;
381   fs->chroot = vroot_fsio_chroot;
382   fs->utimes = vroot_fsio_utimes;
383   fs->opendir = vroot_fsio_opendir;
384   fs->readdir = vroot_fsio_readdir;
385   fs->closedir = vroot_fsio_closedir;
386   fs->mkdir = vroot_fsio_mkdir;
387   fs->rmdir = vroot_fsio_rmdir;
388 
389   vroot_engine = TRUE;
390   return PR_DECLINED(cmd);
391 }
392 
vroot_post_pass(cmd_rec * cmd)393 MODRET vroot_post_pass(cmd_rec *cmd) {
394   if (vroot_engine == TRUE) {
395 
396     /* If not chrooted, unregister vroot. */
397     if (session.chroot_path == NULL) {
398       if (pr_unregister_fs("/") < 0) {
399         pr_log_debug(DEBUG2, MOD_VROOT_VERSION
400           ": error unregistering vroot: %s", strerror(errno));
401 
402       } else {
403         pr_log_debug(DEBUG5, MOD_VROOT_VERSION ": vroot unregistered");
404         pr_fs_setcwd(pr_fs_getvwd());
405         pr_fs_clear_cache();
406       }
407 
408     } else {
409       config_rec *c;
410 
411       /* Otherwise, lookup and process any VRootOptions. */
412       c = find_config(main_server->conf, CONF_PARAM, "VRootOptions", FALSE);
413       if (c != NULL) {
414         vroot_opts = *((unsigned int *) c->argv[0]);
415       }
416 
417       /* XXX This needs to be in the PRE_CMD PASS handler, as when
418        * VRootServer is used, so that a real chroot(2) occurs.
419        */
420       handle_vrootaliases();
421     }
422   }
423 
424   return PR_DECLINED(cmd);
425 }
426 
vroot_post_pass_err(cmd_rec * cmd)427 MODRET vroot_post_pass_err(cmd_rec *cmd) {
428   if (vroot_engine == TRUE) {
429     /* If not chrooted, unregister vroot. */
430     if (session.chroot_path == NULL) {
431       if (pr_unregister_fs("/") < 0) {
432         pr_log_debug(DEBUG2, MOD_VROOT_VERSION
433           ": error unregistering vroot: %s", strerror(errno));
434 
435       } else {
436         pr_log_debug(DEBUG5, MOD_VROOT_VERSION ": vroot unregistered");
437       }
438     }
439 
440     vroot_alias_free();
441   }
442 
443   return PR_DECLINED(cmd);
444 }
445 
446 /* Event listeners
447  */
448 
vroot_exit_ev(const void * event_data,void * user_data)449 static void vroot_exit_ev(const void *event_data, void *user_data) {
450   vroot_alias_free();
451   vroot_fsio_free();
452 }
453 
454 /* Initialization routines
455  */
456 
vroot_sess_init(void)457 static int vroot_sess_init(void) {
458   config_rec *c;
459 
460   c = find_config(main_server->conf, CONF_PARAM, "VRootLog", FALSE);
461   if (c != NULL) {
462     const char *path;
463 
464     path = c->argv[0];
465     if (strcasecmp(path, "none") != 0) {
466       int res, xerrno;
467 
468       PRIVS_ROOT
469       res = pr_log_openfile(path, &vroot_logfd, 0660);
470       xerrno = errno;
471       PRIVS_RELINQUISH
472 
473       switch (res) {
474         case 0:
475           break;
476 
477         case -1:
478           pr_log_debug(DEBUG1, MOD_VROOT_VERSION
479             ": unable to open VRootLog '%s': %s", path, strerror(xerrno));
480           break;
481 
482         case PR_LOG_SYMLINK:
483           pr_log_debug(DEBUG1, MOD_VROOT_VERSION
484             ": unable to open VRootLog '%s': %s", path, "is a symlink");
485           break;
486 
487         case PR_LOG_WRITABLE_DIR:
488           pr_log_debug(DEBUG1, MOD_VROOT_VERSION
489             ": unable to open VRootLog '%s': %s", path,
490             "parent directory is world-writable");
491           break;
492       }
493     }
494   }
495 
496   vroot_alias_init(session.pool);
497   vroot_fsio_init(session.pool);
498   pr_event_register(&vroot_module, "core.exit", vroot_exit_ev, NULL);
499 
500   return 0;
501 }
502 
503 /* Module API tables
504  */
505 
506 static conftable vroot_conftab[] = {
507   { "VRootAlias",	set_vrootalias,		NULL },
508   { "VRootEngine",	set_vrootengine,	NULL },
509   { "VRootLog",		set_vrootlog,		NULL },
510   { "VRootOptions",	set_vrootoptions,	NULL },
511   { "VRootServerRoot",	set_vrootserverroot,	NULL },
512   { NULL }
513 };
514 
515 static cmdtable vroot_cmdtab[] = {
516   { PRE_CMD,		C_PASS,	G_NONE,	vroot_pre_pass, FALSE, FALSE },
517   { POST_CMD,		C_PASS,	G_NONE,	vroot_post_pass, FALSE, FALSE },
518   { POST_CMD_ERR,	C_PASS,	G_NONE,	vroot_post_pass_err, FALSE, FALSE },
519 
520   { PRE_CMD,		C_MKD,	G_NONE,	vroot_pre_mkd, FALSE, FALSE },
521   { POST_CMD,		C_MKD,	G_NONE,	vroot_post_mkd, FALSE, FALSE },
522   { POST_CMD_ERR,	C_MKD,	G_NONE,	vroot_post_mkd, FALSE, FALSE },
523   { PRE_CMD,		C_XMKD,	G_NONE,	vroot_pre_mkd, FALSE, FALSE },
524   { POST_CMD,		C_XMKD,	G_NONE,	vroot_post_mkd, FALSE, FALSE },
525   { POST_CMD_ERR,	C_XMKD,	G_NONE,	vroot_post_mkd, FALSE, FALSE },
526 
527   /* These command handlers are for manipulating cmd->notes, to get
528    * paths properly logged.
529    *
530    * Ideally these would be LOG_CMD/LOG_CMD_ERR phase handlers.  HOWEVER,
531    * we need to transform things before the cmd is dispatched to mod_log,
532    * and mod_log uses a C_ANY handler for logging.  And when dispatching,
533    * C_ANY handlers are run before named handlers.  This means that using
534    * LOG_CMD/LOG_CMD_ERR handlers would be run AFTER mod_log's handler,
535    * even though we appear BEFORE mod_log in the module load order.
536    *
537    * Thus to do the transformation, we actually use CMD/POST_CMD_ERR phase
538    * handlers here.
539    */
540   { CMD,		C_APPE,	G_NONE, vroot_log_stor, FALSE, FALSE },
541   { POST_CMD_ERR,	C_APPE,	G_NONE, vroot_log_stor, FALSE, FALSE },
542   { CMD,		C_RETR,	G_NONE, vroot_log_retr, FALSE, FALSE },
543   { POST_CMD_ERR,	C_RETR,	G_NONE, vroot_log_retr, FALSE, FALSE },
544   { CMD,		C_STOR,	G_NONE, vroot_log_stor, FALSE, FALSE },
545   { POST_CMD_ERR,	C_STOR,	G_NONE, vroot_log_stor, FALSE, FALSE },
546 
547   { 0, NULL }
548 };
549 
550 module vroot_module = {
551   NULL, NULL,
552 
553   /* Module API version 2.0 */
554   0x20,
555 
556   /* Module name */
557   "vroot",
558 
559   /* Module configuration handler table */
560   vroot_conftab,
561 
562   /* Module command handler table */
563   vroot_cmdtab,
564 
565   /* Module authentication handler table */
566   NULL,
567 
568   /* Module initialization function */
569   NULL,
570 
571   /* Session initialization function */
572   vroot_sess_init,
573 
574   /* Module version */
575   MOD_VROOT_VERSION
576 };
577