Repository: borgbackup/borg

Issue:
specify options NOT to enforce/override via borg serve 
== whitelist options determined client-side (and everything else is enforced server-side) - this avoids such security issues.

See there: https://github.com/borgbackup/borg/pull/4095#pullrequestreview-162266367

---

:moneybag: [there is a bounty for this](https://www.bountysource.com/issues/64379886-specify-options-not-to-enforce-override-via-borg-serve)



Discussion:
Hi, probably this is simple, but I want to be sure that I understand it -
edit: strike that. I am just not sure yet what is the closed and what the open part in fail close/open with regard to this. As only expected options are explicitly specified, isn't this already whitelisting?
the point is just that using a whitelist will cause limited functionality if we forget to add something to it (the new stuff won't work).

forgetting to add something to a blacklist causes a security issue though (client being able to use something they should not be able to use).
@step21 are you still working on that?
Yes, but I somehow got sidetracked, sorry. Maybe I'll get something done till the end of the week.
@step ping :)
Looking at this right now...
Going to have a whitelist *and* a blacklist.

The latter will make sure known-critical stuff (which must be enforced server-side) is not accidentally whitelisted.

Resolving patch:
diff --git a/src/borg/archiver.py b/src/borg/archiver.py
--- a/src/borg/archiver.py
+++ b/src/borg/archiver.py
@@ -4294,20 +4294,43 @@ def get_args(self, argv, cmd):
         """usually, just returns argv, except if we deal with a ssh forced command for borg serve."""
         result = self.parse_args(argv[1:])
         if cmd is not None and result.func == self.do_serve:
-            forced_result = result
-            argv = shlex.split(cmd)
+            # borg serve case:
+            # - "result" is how borg got invoked (e.g. via forced command from authorized_keys),
+            # - "client_result" (from "cmd") refers to the command the client wanted to execute,
+            #   which might be different in the case of a forced command or same otherwise.
+            client_argv = shlex.split(cmd)
             # Drop environment variables (do *not* interpret them) before trying to parse
             # the borg command line.
-            argv = list(itertools.dropwhile(lambda arg: '=' in arg, argv))
-            result = self.parse_args(argv[1:])
-            if result.func != forced_result.func:
-                # someone is trying to execute a different borg subcommand, don't do that!
-                return forced_result
-            # we only take specific options from the forced "borg serve" command:
-            result.restrict_to_paths = forced_result.restrict_to_paths
-            result.restrict_to_repositories = forced_result.restrict_to_repositories
-            result.append_only = forced_result.append_only
-            result.storage_quota = forced_result.storage_quota
+            client_argv = list(itertools.dropwhile(lambda arg: '=' in arg, client_argv))
+            client_result = self.parse_args(client_argv[1:])
+            if client_result.func == result.func:
+                # make sure we only process like normal if the client is executing
+                # the same command as specified in the forced command, otherwise
+                # just skip this block and return the forced command (== result).
+                # client is allowed to specify the whitelisted options,
+                # everything else comes from the forced "borg serve" command (or the defaults).
+                # stuff from blacklist must never be used from the client.
+                blacklist = {
+                    'restrict_to_paths',
+                    'restrict_to_repositories',
+                    'append_only',
+                    'storage_quota',
+                }
+                whitelist = {
+                    'debug_topics',
+                    'lock_wait',
+                    'log_level',
+                    'umask',
+                }
+                not_present = object()
+                for attr_name in whitelist:
+                    assert attr_name not in blacklist, 'whitelist has blacklisted attribute name %s' % attr_name
+                    value = getattr(client_result, attr_name, not_present)
+                    if value is not not_present:
+                        # note: it is not possible to specify a whitelisted option via a forced command,
+                        # it always gets overridden by the value specified (or defaulted to) by the client commmand.
+                        setattr(result, attr_name, value)
+
         return result
 
     def parse_args(self, args=None):


Test patch: