We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Possible path confusion / traversal via imprecise store.verify()
The method `store.verify()` checks file paths provided via URL and other ways and raises an exception if they are not matching the validation criteria.

A problem with this validation process was spotted: `os.path.commonprefix()` is not sufficient to check if the path is inside the configured store path. It only compares character by character. Thus allows to navigate into another folder when they share the same start string.

```
Example:     config.STORE_DIR = '/opt/store'
PoC:         store.verify('/opt/store_backup')
```

Mitigation has to make sure, that the path is inside the configured store folder. A mitigation could be to add another check in `store.verify()` with `os.path.relpath(p, config.STORE_DIR)`. If the absolute path p is not inside the store directory, `os.path.relpath()` will return a string starting with '../'.

Example:

```
os.path.relpath('/opt/store_backup', config.STORE_DIR) == '../store_backup'
```

**Reported as part of the cure53 audit of 0.2 as: SD-01-006**


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `securedrop/store.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 import os
3 import re
4 import config
5 import zipfile
6 import crypto_util
7 import uuid
8 import tempfile
9 
10 VALIDATE_FILENAME = re.compile(
11     "^(reply-)?[a-f0-9-]+(_msg|_doc\.zip|)\.gpg$").match
12 
13 
14 class PathException(Exception):
15 
16     '''An exception raised by `store.verify` when it encounters a bad path. A path
17     can be bad when it is not absolute, not normalized, not within
18     `config.STORE_DIR`, or doesn't match the filename format.
19     '''
20     pass
21 
22 
23 def verify(p):
24     '''Assert that the path is absolute, normalized, inside `config.STORE_DIR`, and
25     matches the filename format.
26     '''
27     if not os.path.isabs(config.STORE_DIR):
28         raise PathException("config.STORE_DIR(%s) is not absolute" % (
29             config.STORE_DIR, ))
30 
31     # os.path.abspath makes the path absolute and normalizes '/foo/../bar' to
32     # '/bar', etc. We have to check that the path is normalized before checking
33     # that it starts with the `config.STORE_DIR` or else a malicious actor could
34     # append a bunch of '../../..' to access files outside of the store.
35     if not p == os.path.abspath(p):
36         raise PathException("The path is not absolute and/or normalized")
37 
38     if os.path.commonprefix([config.STORE_DIR, p]) != config.STORE_DIR:
39         raise PathException("Invalid directory %s" % (p, ))
40 
41     filename = os.path.basename(p)
42     ext = os.path.splitext(filename)[-1]
43 
44     if os.path.isfile(p):
45         if filename == '_FLAG':
46             return True
47         if ext != '.gpg':
48             # if there's an extension, verify it's a GPG
49             raise PathException("Invalid file extension %s" % (ext, ))
50         if not VALIDATE_FILENAME(filename):
51             raise PathException("Invalid filename %s" % (filename, ))
52 
53 
54 def path(*s):
55     '''Get the normalized, absolute file path, within `config.STORE_DIR`.'''
56     joined = os.path.join(os.path.abspath(config.STORE_DIR), *s)
57     absolute = os.path.abspath(joined)
58     verify(absolute)
59     return absolute
60 
61 
62 def get_bulk_archive(filenames):
63     zip_file = tempfile.NamedTemporaryFile(prefix='tmp_securedrop_bulk_dl_')
64     with zipfile.ZipFile(zip_file, 'w') as zip:
65         for filename in filenames:
66             verify(filename)
67             zip.write(filename, arcname=os.path.basename(filename))
68     return zip_file
69 
70 
71 def log(msg):
72     file(path('NOTES'), 'a').write(msg)
73
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/securedrop/store.py b/securedrop/store.py
--- a/securedrop/store.py
+++ b/securedrop/store.py
@@ -35,13 +35,13 @@
     if not p == os.path.abspath(p):
         raise PathException("The path is not absolute and/or normalized")
 
-    if os.path.commonprefix([config.STORE_DIR, p]) != config.STORE_DIR:
+    # Check that the path p is in config.STORE_DIR
+    if os.path.relpath(p, config.STORE_DIR).startswith('..'):
         raise PathException("Invalid directory %s" % (p, ))
 
-    filename = os.path.basename(p)
-    ext = os.path.splitext(filename)[-1]
-
     if os.path.isfile(p):
+        filename = os.path.basename(p)
+        ext = os.path.splitext(filename)[-1]
         if filename == '_FLAG':
             return True
         if ext != '.gpg':

{"golden_diff": "diff --git a/securedrop/store.py b/securedrop/store.py\n--- a/securedrop/store.py\n+++ b/securedrop/store.py\n@@ -35,13 +35,13 @@\n     if not p == os.path.abspath(p):\n         raise PathException(\"The path is not absolute and/or normalized\")\n \n-    if os.path.commonprefix([config.STORE_DIR, p]) != config.STORE_DIR:\n+    # Check that the path p is in config.STORE_DIR\n+    if os.path.relpath(p, config.STORE_DIR).startswith('..'):\n         raise PathException(\"Invalid directory %s\" % (p, ))\n \n-    filename = os.path.basename(p)\n-    ext = os.path.splitext(filename)[-1]\n-\n     if os.path.isfile(p):\n+        filename = os.path.basename(p)\n+        ext = os.path.splitext(filename)[-1]\n         if filename == '_FLAG':\n             return True\n         if ext != '.gpg':\n", "issue": "Possible path confusion / traversal via imprecise store.verify()\nThe method `store.verify()` checks file paths provided via URL and other ways and raises an exception if they are not matching the validation criteria.\n\nA problem with this validation process was spotted: `os.path.commonprefix()` is not sufficient to check if the path is inside the configured store path. It only compares character by character. Thus allows to navigate into another folder when they share the same start string.\n\n```\nExample:     config.STORE_DIR = '/opt/store'\nPoC:         store.verify('/opt/store_backup')\n```\n\nMitigation has to make sure, that the path is inside the configured store folder. A mitigation could be to add another check in `store.verify()` with `os.path.relpath(p, config.STORE_DIR)`. If the absolute path p is not inside the store directory, `os.path.relpath()` will return a string starting with '../'.\n\nExample:\n\n```\nos.path.relpath('/opt/store_backup', config.STORE_DIR) == '../store_backup'\n```\n\n**Reported as part of the cure53 audit of 0.2 as: SD-01-006**\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\nimport os\nimport re\nimport config\nimport zipfile\nimport crypto_util\nimport uuid\nimport tempfile\n\nVALIDATE_FILENAME = re.compile(\n    \"^(reply-)?[a-f0-9-]+(_msg|_doc\\.zip|)\\.gpg$\").match\n\n\nclass PathException(Exception):\n\n    '''An exception raised by `store.verify` when it encounters a bad path. A path\n    can be bad when it is not absolute, not normalized, not within\n    `config.STORE_DIR`, or doesn't match the filename format.\n    '''\n    pass\n\n\ndef verify(p):\n    '''Assert that the path is absolute, normalized, inside `config.STORE_DIR`, and\n    matches the filename format.\n    '''\n    if not os.path.isabs(config.STORE_DIR):\n        raise PathException(\"config.STORE_DIR(%s) is not absolute\" % (\n            config.STORE_DIR, ))\n\n    # os.path.abspath makes the path absolute and normalizes '/foo/../bar' to\n    # '/bar', etc. We have to check that the path is normalized before checking\n    # that it starts with the `config.STORE_DIR` or else a malicious actor could\n    # append a bunch of '../../..' to access files outside of the store.\n    if not p == os.path.abspath(p):\n        raise PathException(\"The path is not absolute and/or normalized\")\n\n    if os.path.commonprefix([config.STORE_DIR, p]) != config.STORE_DIR:\n        raise PathException(\"Invalid directory %s\" % (p, ))\n\n    filename = os.path.basename(p)\n    ext = os.path.splitext(filename)[-1]\n\n    if os.path.isfile(p):\n        if filename == '_FLAG':\n            return True\n        if ext != '.gpg':\n            # if there's an extension, verify it's a GPG\n            raise PathException(\"Invalid file extension %s\" % (ext, ))\n        if not VALIDATE_FILENAME(filename):\n            raise PathException(\"Invalid filename %s\" % (filename, ))\n\n\ndef path(*s):\n    '''Get the normalized, absolute file path, within `config.STORE_DIR`.'''\n    joined = os.path.join(os.path.abspath(config.STORE_DIR), *s)\n    absolute = os.path.abspath(joined)\n    verify(absolute)\n    return absolute\n\n\ndef get_bulk_archive(filenames):\n    zip_file = tempfile.NamedTemporaryFile(prefix='tmp_securedrop_bulk_dl_')\n    with zipfile.ZipFile(zip_file, 'w') as zip:\n        for filename in filenames:\n            verify(filename)\n            zip.write(filename, arcname=os.path.basename(filename))\n    return zip_file\n\n\ndef log(msg):\n    file(path('NOTES'), 'a').write(msg)\n", "path": "securedrop/store.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\nimport os\nimport re\nimport config\nimport zipfile\nimport crypto_util\nimport uuid\nimport tempfile\n\nVALIDATE_FILENAME = re.compile(\n    \"^(reply-)?[a-f0-9-]+(_msg|_doc\\.zip|)\\.gpg$\").match\n\n\nclass PathException(Exception):\n\n    '''An exception raised by `store.verify` when it encounters a bad path. A path\n    can be bad when it is not absolute, not normalized, not within\n    `config.STORE_DIR`, or doesn't match the filename format.\n    '''\n    pass\n\n\ndef verify(p):\n    '''Assert that the path is absolute, normalized, inside `config.STORE_DIR`, and\n    matches the filename format.\n    '''\n    if not os.path.isabs(config.STORE_DIR):\n        raise PathException(\"config.STORE_DIR(%s) is not absolute\" % (\n            config.STORE_DIR, ))\n\n    # os.path.abspath makes the path absolute and normalizes '/foo/../bar' to\n    # '/bar', etc. We have to check that the path is normalized before checking\n    # that it starts with the `config.STORE_DIR` or else a malicious actor could\n    # append a bunch of '../../..' to access files outside of the store.\n    if not p == os.path.abspath(p):\n        raise PathException(\"The path is not absolute and/or normalized\")\n\n    # Check that the path p is in config.STORE_DIR\n    if os.path.relpath(p, config.STORE_DIR).startswith('..'):\n        raise PathException(\"Invalid directory %s\" % (p, ))\n\n    if os.path.isfile(p):\n        filename = os.path.basename(p)\n        ext = os.path.splitext(filename)[-1]\n        if filename == '_FLAG':\n            return True\n        if ext != '.gpg':\n            # if there's an extension, verify it's a GPG\n            raise PathException(\"Invalid file extension %s\" % (ext, ))\n        if not VALIDATE_FILENAME(filename):\n            raise PathException(\"Invalid filename %s\" % (filename, ))\n\n\ndef path(*s):\n    '''Get the normalized, absolute file path, within `config.STORE_DIR`.'''\n    joined = os.path.join(os.path.abspath(config.STORE_DIR), *s)\n    absolute = os.path.abspath(joined)\n    verify(absolute)\n    return absolute\n\n\ndef get_bulk_archive(filenames):\n    zip_file = tempfile.NamedTemporaryFile(prefix='tmp_securedrop_bulk_dl_')\n    with zipfile.ZipFile(zip_file, 'w') as zip:\n        for filename in filenames:\n            verify(filename)\n            zip.write(filename, arcname=os.path.basename(filename))\n    return zip_file\n\n\ndef log(msg):\n    file(path('NOTES'), 'a').write(msg)\n", "path": "securedrop/store.py"}]}