Repository: iterative/dvc

Issue:
run/repro: check that --cwd is inside the repository
macOS High Sierra 10.13.6 (17G65)
```
$ python --version
Python 3.6.6
```

Reproduce MWE:

Clean installation:

```
$ virtualenv venv --python=python3.6
$ source venv/bin/activate
$ pip install dvc==0.19.13
$ git init
$ dvc init
```

Minimal example that works:

```
$ dvc run -o /tmp/input.txt `echo "Test" > /tmp/input.txt`
$ dvc run -d /tmp/input.txt -o /tmp/output.txt `wc /tmp/input.txt > /tmp/output.txt`
$ dvc repro output.txt.dvc
```

Minimal example that crashes:

```
$ dvc run --cwd /tmp -o /tmp/input.txt `echo "Test" > /tmp/input.txt`
$ dvc run --cwd /tmp -d /tmp/input.txt -o /tmp/output.txt `wc /tmp/input.txt > /tmp/output.txt`
$ dvc repro --cwd /tmp /tmp/output.txt.dvc
Error: Unexpected error: '../../../../tmp/output.txt.dvc'
```


Discussion:
Hi @habernal !

Sorry for a delay. I suppose /tmp is outside of your project directory, as well as /tmp/output.txt.dvc, which is where the error comes. You should place your dvc files inside your project only and --cwd should point to a directory inside your project. We need to add a check for that. Will send a patch soon.

Thank you for the feedback!
@efiop Thanks for clarifying, yes, the /tmp is outside of the project directory. I believe the fastest fix is to simply document it in the reference guide.
Agreed. I'll update help messages along with the command refs in the docs. Thank you for the feedback!

Resolving patch:
diff --git a/dvc/stage.py b/dvc/stage.py
--- a/dvc/stage.py
+++ b/dvc/stage.py
@@ -40,6 +40,12 @@ def __init__(self, msg):
         super(StageFileBadNameError, self).__init__(msg)
 
 
+class StageBadCwdError(DvcException):
+    def __init__(self, cwd):
+        msg = "Stage cwd '{}' is outside of the current dvc project"
+        super(StageBadCwdError, self).__init__(msg.format(cwd))
+
+
 class MissingDep(DvcException):
     def __init__(self, deps):
         assert len(deps) > 0
@@ -275,6 +281,13 @@ def _stage_fname_cwd(cls, fname, cwd, outs, add):
 
         return (fname, cwd)
 
+    @staticmethod
+    def _check_inside_project(project, cwd):
+        assert project is not None
+        proj_dir = os.path.realpath(project.root_dir)
+        if not os.path.realpath(cwd).startswith(proj_dir):
+            raise StageBadCwdError(cwd)
+
     @staticmethod
     def loads(project=None,
               cmd=None,
@@ -305,6 +318,8 @@ def loads(project=None,
 
         fname, cwd = Stage._stage_fname_cwd(fname, cwd, stage.outs, add=add)
 
+        Stage._check_inside_project(project, cwd)
+
         cwd = os.path.abspath(cwd)
         path = os.path.join(cwd, fname)
 


Test patch:
diff --git a/tests/test_run.py b/tests/test_run.py
--- a/tests/test_run.py
+++ b/tests/test_run.py
@@ -4,6 +4,7 @@
 from dvc.main import main
 from dvc.utils import file_md5
 from dvc.stage import Stage, StageFileBadNameError, MissingDep
+from dvc.stage import StageBadCwdError
 from dvc.command.run import CmdRun
 from dvc.exceptions import OutputDuplicationError, CircularDependencyError
 
@@ -101,6 +102,12 @@ def test(self):
                          outs=[self.FOO],
                          fname='circular-dependency.dvc')
 
+class TestRunBadCwd(TestDvc):
+    def test(self):
+        with self.assertRaises(StageBadCwdError):
+            self.dvc.run(cmd='',
+                         cwd=self.mkdtemp())
+
 
 class TestCmdRun(TestDvc):
     def test_run(self):