From b6ee149217dea8eb9b6c4a4410c8d0b97bf64e8e Mon Sep 17 00:00:00 2001 From: Michal Rakowski Date: Mon, 20 Sep 2021 21:23:34 +0200 Subject: [PATCH] Add 'AllowedBackupDir' FD directive --- bacula/src/baconfig.h | 18 +++++----- bacula/src/filed/fd_plugins.c | 2 +- bacula/src/filed/filed_conf.c | 5 +++ bacula/src/filed/filed_conf.h | 1 + bacula/src/filed/job.c | 49 +++++++++++++++++++++++++ bacula/src/findlib/find.c | 68 +++++++++++++++++++++++++++++++++-- bacula/src/findlib/find.h | 1 + bacula/src/findlib/protos.h | 2 +- bacula/src/jcr.h | 1 + 9 files changed, 134 insertions(+), 13 deletions(-) diff --git a/bacula/src/baconfig.h b/bacula/src/baconfig.h index 291e5ac0a..59542bd63 100644 --- a/bacula/src/baconfig.h +++ b/bacula/src/baconfig.h @@ -583,14 +583,16 @@ int m_msg(const char *file, int line, POOLMEM **msgbuf, const char *fmt,...); int m_msg(const char *file, int line, POOLMEM *&pool_buf, const char *fmt, ...); void t_msg(const char *file, int line, int64_t level, const char *fmt,...); -/* To be fully implemented. To use bstrncpy(), then it's only when we - * want to copy up to the container size, sometime strcpy() is used - * with a number of bytes to copy, then the result is incorrect. - * ex: char buf[200]; bstrncpy(buf, "hello", strlen("hello")); buf -> "hell" - * - * Use bstrncpy instead of strncpy because it ensures last char is a 0 - */ -/* #define strncpy bad_call_on_strncpy_use_bstrncpy */ +/* Helper macro e.g. for comparison of file patch with allowed directory + * On Linux/Unix OSes, paths are case sensitive while on Windows it is not the case */ +#ifdef HAVE_WIN32 +#define b_path_match strstr +#else +#define b_path_match strcasestr +#endif // HAVE_WIN32 + +/* Use bstrncpy instead of strncpy because it ensures last char is a 0 */ +#define strncpy bad_call_on_strncpy_use_bstrncpy /** Use our strdup with smartalloc */ #ifdef HAVE_WXCONSOLE diff --git a/bacula/src/filed/fd_plugins.c b/bacula/src/filed/fd_plugins.c index afbaa2ba1..b05795967 100644 --- a/bacula/src/filed/fd_plugins.c +++ b/bacula/src/filed/fd_plugins.c @@ -2523,7 +2523,7 @@ static bRC baculaAcceptFile(bpContext *ctx, struct save_pkt *sp) ff_pkt->fname = sp->fname; ff_pkt->statp = sp->statp; - if (accept_file(ff_pkt)) { + if (accept_file(jcr, ff_pkt)) { ret = bRC_OK; } else { ret = bRC_Skip; diff --git a/bacula/src/filed/filed_conf.c b/bacula/src/filed/filed_conf.c index 882ec8986..24e2e7fd4 100644 --- a/bacula/src/filed/filed_conf.c +++ b/bacula/src/filed/filed_conf.c @@ -156,6 +156,7 @@ static RES_ITEM dir_items[] = { {"ConnectToDirector", store_bool, ITEM(res_dir.connect_to_dir), 0, 0, 0}, {"Schedule", store_res, ITEM(res_dir.schedule), R_SCHEDULE, 0, 0}, {"ReconnectionTime", store_time,ITEM(res_dir.reconnection_time), 0, ITEM_DEFAULT, 60 * 45}, + {"AllowedBackupDirectories", store_alist_str, ITEM(res_dir.allowed_backup_dirs), 0, 0, 0}, {NULL, NULL, {0}, 0, 0, 0} }; @@ -536,6 +537,9 @@ void free_resource(RES *sres, int type) if (res->res_dir.disabled_cmds_array) { free(res->res_dir.disabled_cmds_array); } + if (res->res_dir.allowed_backup_dirs) { + delete res->res_dir.allowed_backup_dirs; + } break; case R_CONSOLE: if (res->res_cons.dirinfo.password) { @@ -776,6 +780,7 @@ bool save_resource(CONFIG *config, int type, RES_ITEM *items, int pass) } res->res_dir.dirinfo.tls_allowed_cns = res_all.res_dir.dirinfo.tls_allowed_cns; res->res_dir.disable_cmds = res_all.res_dir.disable_cmds; + res->res_dir.allowed_backup_dirs = res_all.res_dir.allowed_backup_dirs; res->res_dir.console = res_all.res_dir.console; res->res_dir.schedule = res_all.res_dir.schedule; break; diff --git a/bacula/src/filed/filed_conf.h b/bacula/src/filed/filed_conf.h index a4acca622..b1b8c7d84 100644 --- a/bacula/src/filed/filed_conf.h +++ b/bacula/src/filed/filed_conf.h @@ -117,6 +117,7 @@ struct DIRRES { CONSRES *console; SCHEDRES *schedule; /* Know when to connect the Director */ int reconnection_time; /* Reconnect after a given time */ + alist *allowed_backup_dirs; /* Allowed to-be-backed-up directory list */ }; struct CLIENT { diff --git a/bacula/src/filed/job.c b/bacula/src/filed/job.c index 9a1beea9f..1d6620f38 100644 --- a/bacula/src/filed/job.c +++ b/bacula/src/filed/job.c @@ -288,6 +288,46 @@ JCR *new_fd_jcr() return jcr; } +static bool setup_allowed_dirs(FF_PKT *ff, alist *directories) +{ + bool ret = true; + char *dir; + + if (!ff->allowed_backup_dirs) { + ff->allowed_backup_dirs = New(alist(10, owned_by_alist)); + } + + POOL_MEM rpath(PM_FNAME); + rpath.check_size(PATH_MAX); + + foreach_alist(dir, directories) { + /* Add resolved directory path to the find packet list */ + ff->allowed_backup_dirs->append(bstrdup(dir)); + } + + return ret; +} + +/* Setup Director-related find files packet fileds. + * Currently supported directive: + * - Allowed Backup Directories + * + * TODO: add Exlude Directories + */ +static bool setup_find_files(JCR *jcr, DIRRES *director) +{ + FF_PKT *ff = jcr->ff; + + if (director->allowed_backup_dirs) { + if (!setup_allowed_dirs(ff, director->allowed_backup_dirs)) { + Jmsg0(jcr, M_WARNING, 0, _("Unable to resolve some of the Allowed Directories.\n")); + return false; + } + } + + return true; +} + /* * Accept requests from a Director * @@ -2405,6 +2445,8 @@ static int estimate_cmd(JCR *jcr) return 0; } + setup_find_files(jcr, jcr->director); + /* On windows, the fileset can be completed by this function (File=/ => File=C:/ + File=E:/) */ get_win32_driveletters(jcr, jcr->ff, NULL, NULL); @@ -2872,6 +2914,8 @@ static int backup_cmd(JCR *jcr) Dmsg1(100, "JobFiles=%ld\n", jcr->JobFiles); } + setup_find_files(jcr, jcr->director); + /* * If explicitly requesting FO_ACL or FO_XATTR, fail job if it * is not available on Client machine @@ -3045,6 +3089,11 @@ static int backup_cmd(JCR *jcr) } } + /* Warn the user if some directories were skipped because of invalid (not-allowed) location */ + if (jcr->num_dirs_skipped) { + Jmsg(jcr, M_WARNING, 0, _("Skipped %d directories which were out of backup allowed directories.\n"), jcr->num_dirs_skipped); + } + cleanup: #if defined(WIN32_VSS) if (jcr->Snapshot) { diff --git a/bacula/src/findlib/find.c b/bacula/src/findlib/find.c index 7410fcdca..b60993dd1 100644 --- a/bacula/src/findlib/find.c +++ b/bacula/src/findlib/find.c @@ -40,6 +40,7 @@ int32_t path_max; /* path name max length */ #undef bmalloc #define bmalloc(x) sm_malloc(__FILE__, __LINE__, x) #endif + static int our_callback(JCR *jcr, FF_PKT *ff, bool top_level); static const int fnmode = 0; @@ -263,8 +264,49 @@ bool is_in_fileset(FF_PKT *ff) return false; } +/** + * Check if the file being processed is inside allowed directories or not. + * + * Returns: true if OK to backup + * false to ignore file/directory + */ +static int check_allowed_dirs(JCR *jcr, FF_PKT *ff_pkt) +{ + bool ret = true; + char *dir, *pp; + + if (ff_pkt->allowed_backup_dirs) { + foreach_alist(dir, ff_pkt->allowed_backup_dirs) { + /* The b_path_match check can be done twice here: + * For the 1st time we check if current file path contains exactly the allowed dir - if it does + * then file/directory can be backed up + * For the 2nd time we check if current file is a part of allowed dir - if it does + * then we know we can descend into directories below to continue checking. + * If it does not then we can skip this file as well as all directories below. + */ + pp = b_path_match(dir, ff_pkt->fname); + if (pp == dir) { + ret = true; + break; + } else if ((pp = b_path_match(ff_pkt->fname, dir)) == ff_pkt->fname) { + ret = true; + break; + } else { + ret = false; + } + } + + if (ret == false && S_ISDIR(ff_pkt->statp.st_mode)) { + Dmsg1(dbglvl, "Skipping directory %s, it's out of allowed ones\n", ff_pkt->fname); + Jmsg(jcr, M_SKIPPED, 0, _("Skipping directory %s, it's out of allowed ones\n"), ff_pkt->fname); + jcr->num_dirs_skipped++; + } + } + + return ret; +} -bool accept_file(FF_PKT *ff) +bool accept_file(JCR *jcr, FF_PKT *ff) { int i, j, k; int fnm_flags; @@ -286,6 +328,10 @@ bool accept_file(FF_PKT *ff) basename = ff->fname; } + if (!check_allowed_dirs(jcr, ff)) { + return false; + } + for (j = 0; j < incexe->opts_list.size(); j++) { findFOPTS *fo = (findFOPTS *)incexe->opts_list.get(j); ff->flags = fo->flags; @@ -432,8 +478,21 @@ bool accept_file(FF_PKT *ff) static int our_callback(JCR *jcr, FF_PKT *ff, bool top_level) { if (top_level) { - return ff->file_save(jcr, ff, top_level); /* accept file */ + /* Check if we want descend at all - we may not want to backup files from the top directory + * but maybe something below */ + if (check_allowed_dirs(jcr, ff)) { + Dmsg1(dbglvl, "Descending into top-level directory %s, it's part of allowed directories paths\n", + ff->fname); + return ff->file_save(jcr, ff, top_level); /* accept file */ + } else { + Dmsg1(dbglvl, "Will not descend into top-level directory %s, " + "it's not within allowed directories paths\n", + ff->fname); + /* Skip top dir and anything below */ + return -1; + } } + switch (ff->type) { case FT_NOACCESS: case FT_NOFOLLOW: @@ -460,7 +519,7 @@ static int our_callback(JCR *jcr, FF_PKT *ff, bool top_level) case FT_DIRNOCHG: case FT_REPARSE: case FT_JUNCTION: - if (accept_file(ff)) { + if (accept_file(jcr, ff)) { return ff->file_save(jcr, ff, top_level); } else { Dmsg1(dbglvl, "Skip file %s\n", ff->fname); @@ -498,6 +557,9 @@ term_find_files(FF_PKT *ff) if (ff->mtab_list) { delete ff->mtab_list; } + if (ff->allowed_backup_dirs) { + delete ff->allowed_backup_dirs; + } hard_links = term_find_one(ff); free(ff); return hard_links; diff --git a/bacula/src/findlib/find.h b/bacula/src/findlib/find.h index 1b118bb2b..fba8c9fe1 100644 --- a/bacula/src/findlib/find.h +++ b/bacula/src/findlib/find.h @@ -160,6 +160,7 @@ struct FF_PKT { POOLMEM *fname_save; /* save when stripping path */ POOLMEM *link_save; /* save when stripping path */ POOLMEM *ignoredir_fname; /* used to ignore directories */ + alist *allowed_backup_dirs; /* List of allowed directories with absolute paths */ char *digest; /* set to file digest when the file is a hardlink */ struct stat statp; /* stat packet */ bool stat_update; /* Only file's metada needds to be updated */ diff --git a/bacula/src/findlib/protos.h b/bacula/src/findlib/protos.h index 1f1afa659..a6fcacfdd 100644 --- a/bacula/src/findlib/protos.h +++ b/bacula/src/findlib/protos.h @@ -45,7 +45,7 @@ int find_files(JCR *jcr, FF_PKT *ff, int file_sub(JCR *, FF_PKT *ff_pkt, bool) int match_files(JCR *jcr, FF_PKT *ff, int sub(JCR *, FF_PKT *ff_pkt, bool)); int term_find_files(FF_PKT *ff); bool is_in_fileset(FF_PKT *ff); -bool accept_file(FF_PKT *ff); +bool accept_file(JCR *jcr, FF_PKT *ff); /* From match.c */ void init_include_exclude_files(FF_PKT *ff); diff --git a/bacula/src/jcr.h b/bacula/src/jcr.h index 57d110dcf..66535ffca 100644 --- a/bacula/src/jcr.h +++ b/bacula/src/jcr.h @@ -272,6 +272,7 @@ public: uint64_t ReadBytes; /* Bytes read -- before compression */ uint64_t CommBytes; /* FD comm line bytes sent to SD */ uint64_t CommCompressedBytes; /* FD comm line compressed bytes sent to SD */ + uint32_t num_dirs_skipped; /* directories skipped by this job (see 'Allowed Backup Dir' directive */ FileId_t FileId; /* Last FileId used */ volatile int32_t JobStatus; /* ready, running, blocked, terminated */ int32_t JobPriority; /* Job priority */ -- 2.47.3