Repository: borgbackup/borg

Issue:
deal better with options passed from client to server
https://github.com/borgbackup/borg/pull/529#discussion_r49457475 see that thread.

---

:moneybag: [there is a bounty for this](https://www.bountysource.com/issues/29821167-deal-better-with-options-passed-from-client-to-server)



Discussion:
As a summary:
- `borg serve` could automatically detect it runs under SSH by checking environment variable `SSH_ORIGINAL_COMMAND` and cleans up/restricts the parameters given by the client.
- possibly borg could be configured to allow/restrict certain parameters as a comma separated list `--exclude-args=restrict-path,umask`.

do i understand this correctly that `borg serve` should check if `SSH_ORIGINAL_COMMAND` exists and then have a whitelist of commandline arguments provided? other than `--restrict-path`, what else should be allowed (`--verbose`) was mentionned, but then there was an objection there as well...

there was a discussion there as well to change directory to what is specified in `--restrict-path` to allow easier relative paths for archives... but then i wonder if it wouldn't be simpler to leave that to a wrapper script as well - if you're going to have a `command` in your `authorized_keys` file (right?), might as well change the path there as well... 

i am not sure this is a necessary change or what is necessary here.

@anarcat the problem is that the server process should be in sync with the client concerning mode of operation. Otherwise stuff done on the server side does not behave as expected when looking at the cli parameters given to the client.

E.g. if you give `borg create --umask 0007 ...` on the client side and the server side uses a hardcoded borg command that does not include that umask, it won't behave as expected from the cli params.
Same for logging level (server might stay at WARNING, although -v/--info was given at client cli).
Some more to check are mention in the original ticket, see first post.

Hard coding this into `authorized_keys` file is not really a solution, as it does not adapt to changing cli options given by the user.

So yes, I guess we need a whitelist for options that are allowed to change.
Or maybe even some special mechanism to one can give that whitelist in the hard coded command by a special placeholder, like `borg --umask=<REMOTE>`. Not sure if that is doable.

I think the "easiest and robust fix" would to just not allow overwriting `restrict-path` when already set, then the commandline args from the client could be directly parsed into the server process by reading `SSH_ORIGINAL_COMMAND` environment variable.


Resolving patch:
diff --git a/borg/archiver.py b/borg/archiver.py
--- a/borg/archiver.py
+++ b/borg/archiver.py
@@ -7,6 +7,7 @@
 import inspect
 import io
 import os
+import shlex
 import signal
 import stat
 import sys
@@ -1332,6 +1333,20 @@ def build_parser(self, args=None, prog=None):
                                help='hex object ID(s) to delete from the repo')
         return parser
 
+    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)
+            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
+            # the only thing we take from the forced "borg serve" ssh command is --restrict-to-path
+            result.restrict_to_paths = forced_result.restrict_to_paths
+        return result
+
     def parse_args(self, args=None):
         # We can't use argparse for "serve" since we don't want it to show up in "Available commands"
         if args:
@@ -1407,7 +1422,7 @@ def main():  # pragma: no cover
     setup_signal_handlers()
     archiver = Archiver()
     msg = None
-    args = archiver.parse_args(sys.argv[1:])
+    args = archiver.get_args(sys.argv, os.environ.get('SSH_ORIGINAL_COMMAND'))
     try:
         exit_code = archiver.run(args)
     except Error as e:
diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py
--- a/borg/testsuite/archiver.py
+++ b/borg/testsuite/archiver.py
@@ -1129,3 +1129,24 @@ def test_fuse_mount_archive(self):
     @unittest.skip('only works locally')
     def test_debug_put_get_delete_obj(self):
         pass
+
+
+def test_get_args():
+    archiver = Archiver()
+    # everything normal:
+    # first param is argv as produced by ssh forced command,
+    # second param is like from SSH_ORIGINAL_COMMAND env variable
+    args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
+                             'borg serve --info --umask=0027')
+    assert args.func == archiver.do_serve
+    assert args.restrict_to_paths == ['/p1', '/p2']
+    assert args.umask == 0o027
+    assert args.log_level == 'info'
+    # trying to cheat - break out of path restriction
+    args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
+                             'borg serve --restrict-to-path=/')
+    assert args.restrict_to_paths == ['/p1', '/p2']
+    # trying to cheat - try to execute different subcommand
+    args = archiver.get_args(['borg', 'serve', '--restrict-to-path=/p1', '--restrict-to-path=/p2', ],
+                             'borg init /')
+    assert args.func == archiver.do_serve


Test patch: