[draft PATCH] whitelist support for refuse options

Nick Cleaton nick at cleaton.net
Sun Feb 9 10:29:10 UTC 2020


This adds support for whitelisting the acceptable options in the
"refuse options" setting in rsyncd.conf. It introduces "!" as a
special option string that refuses most options and interprets
any following strings as patterns of options to allow.

For example, to allow only verbose and archive:

  refuse options = ! verbose archive

The "!" does't refuse no-iconv, but you can still refuse it and
use a whitelist if you want:

  refuse options = no-iconv ! verbose archive

It's not finished (needs tests and doc) I just wanted to see if
there'd be any interest in merging something of this shape
before I put more work into it.

My use case is setting up a restricted trust relationship by
allowing host A to ssh to host B with a forced command of
"rsync --server --daemon --config=/path/to/rsyncd.conf ." and
configuring the restictions in rsyncd.conf. I know what options
I want to use, it'd be nice to enforce that on the server side
without listing every other option in "refuse options".


---
 options.c | 114 +++++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 82 insertions(+), 32 deletions(-)

diff --git a/options.c b/options.c
index e5b0cb68..02d1b174 100644
--- a/options.c
+++ b/options.c
@@ -1133,39 +1133,101 @@ static void set_refuse_options(char *bp)
 {
 	struct poptOption *op;
 	char *cp, shortname[2];
-	int is_wild, found_match;
+	int is_wild, found_match, whitelist_mode, archive_whitelisted;
 
 	shortname[1] = '\0';
+	whitelist_mode = 0;
+	archive_whitelisted = 0;
 
+	/* We flag options for refusal by abusing the "descrip" field of
+	 * struct poptOption (which we don't use) to temporarily store
+	 * a refuse flag. Refused options may be un-refused later in the
+	 * loop if whitelist mode is triggered. */
 	while (1) {
 		while (*bp == ' ') bp++;
 		if (!*bp)
 			break;
 		if ((cp = strchr(bp, ' ')) != NULL)
 			*cp= '\0';
-		is_wild = strpbrk(bp, "*?[") != NULL;
-		found_match = 0;
+		if (!strcmp(bp, "!")) {
+			whitelist_mode = 1;
+			for (op = long_options; ; op++) {
+				*shortname = op->shortName;
+				if (!op->longName && !*shortname)
+					break;
+				if (*shortname != 'e' && (!op->longName ||(
+				    strcmp("server", op->longName) &&
+				    strcmp("sender", op->longName) &&
+				    strcmp("no-iconv", op->longName))))
+					op->descrip = "refused";
+			}
+		} else {
+			is_wild = strpbrk(bp, "*?[") != NULL;
+			found_match = 0;
+			for (op = long_options; ; op++) {
+				*shortname = op->shortName;
+				if (!op->longName && !*shortname)
+					break;
+				if ((op->longName && wildmatch(bp, op->longName))
+				    || (*shortname && wildmatch(bp, shortname))) {
+					op->descrip = whitelist_mode ? 0 : "refused";
+					found_match = 1;
+					if (whitelist_mode && *shortname == 'a')
+						archive_whitelisted = 1;
+					if (!is_wild)
+						break;
+				}
+			}
+			if (!found_match) {
+				rprintf(FLOG, "No match for refuse-options string \"%s\"\n",
+					bp);
+			}
+		}
+		if (!cp)
+			break;
+		*cp = ' ';
+		bp = cp + 1;
+	}
+
+	/* For the --archive option, the client sends the implied options
+	 * explicitly to the server, so if --archive is whitelisted then
+	 * we must individually whitelist the implied options as well. */
+	if (archive_whitelisted) {
 		for (op = long_options; ; op++) {
 			*shortname = op->shortName;
 			if (!op->longName && !*shortname)
 				break;
-			if ((op->longName && wildmatch(bp, op->longName))
-			    || (*shortname && wildmatch(bp, shortname))) {
-				if (op->argInfo == POPT_ARG_VAL)
-					op->argInfo = POPT_ARG_NONE;
-				op->val = (op - long_options) + OPT_REFUSED_BASE;
-				found_match = 1;
-				/* These flags are set to let us easily check
-				 * an implied option later in the code. */
-				switch (*shortname) {
-				case 'r': case 'd': case 'l': case 'p':
-				case 't': case 'g': case 'o': case 'D':
-					refused_archive_part = op->val;
-					break;
-				case 'z':
+			if (*shortname && strchr("rdlptgoD", *shortname))
+				op->descrip = 0;
+		}
+	}
+
+	/* The actual mechanics of marking options for refusal, now
+	 * that the set of refused options is finalized. */
+	for (op = long_options; ; op++) {
+		*shortname = op->shortName;
+		if (!op->longName && !*shortname)
+			break;
+		if (op->descrip) {
+			op->descrip = 0;
+			if (op->argInfo == POPT_ARG_VAL)
+				op->argInfo = POPT_ARG_NONE;
+			op->val = (op - long_options) + OPT_REFUSED_BASE;
+			/* These flags are set to let us easily check
+			 * an implied option later in the code. */
+			switch (*shortname) {
+			case 'r': case 'd': case 'l': case 'p':
+			case 't': case 'g': case 'o': case 'D':
+				refused_archive_part = op->val;
+				break;
+			case 'z':
+				if (!whitelist_mode)
 					refused_compress = op->val;
-					break;
-				case '\0':
+				break;
+			case '\0':
+				if (wildmatch("no-iconv", op->longName))
+					refused_no_iconv = op->val;
+				else if (!whitelist_mode) {
 					if (wildmatch("delete", op->longName))
 						refused_delete = op->val;
 					else if (wildmatch("delete-before", op->longName))
@@ -1178,22 +1240,10 @@ static void set_refuse_options(char *bp)
 						refused_progress = op->val;
 					else if (wildmatch("inplace", op->longName))
 						refused_inplace = op->val;
-					else if (wildmatch("no-iconv", op->longName))
-						refused_no_iconv = op->val;
-					break;
 				}
-				if (!is_wild)
-					break;
+				break;
 			}
 		}
-		if (!found_match) {
-			rprintf(FLOG, "No match for refuse-options string \"%s\"\n",
-				bp);
-		}
-		if (!cp)
-			break;
-		*cp = ' ';
-		bp = cp + 1;
 	}
 }
 
-- 
2.17.1



More information about the rsync mailing list