prompt: 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()
  
  ```
  
golden_diff: 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':
  
verification_info: {"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"}]}