Skip to content

Commit

Permalink
Merge branch 'tseaver-jp_exploit_fix'
Browse files Browse the repository at this point in the history
  • Loading branch information
mmerickel committed Aug 25, 2023
2 parents 0919da5 + 6726314 commit b4e78bd
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 5 deletions.
13 changes: 13 additions & 0 deletions CHANGES.rst
Expand Up @@ -14,9 +14,22 @@ Features
Bug Fixes
---------

- Removed support for null-bytes in the path when making a request for a file
against a static_view. Whille null-bytes are allowed by the HTTP
specification, due to the handling of null-bytes potentially leading to
security vulnerabilities it is no longer supported.

This fixes a security vulnerability that is present due to a bug in Python
3.11.0 through 3.11.4, thereby allowing the unintended disclosure of an
``index.html`` one directory up from the static views path.

Thanks to Masashi Yamane of LAC Co., Ltd for reporting this issue.

Backward Incompatibilities
--------------------------

- Requests to a static_view are no longer allowed to contain a null-byte in any
part of the path segment.
- Pyramid is no longer tested on, nor supports Python 3.6
- Pyramid drops support for l*gettext() methods in the i18n module.
These have been deprecated in Python's gettext module since 3.8, and
Expand Down
10 changes: 5 additions & 5 deletions src/pyramid/static.py
Expand Up @@ -260,12 +260,12 @@ def _add_vary(response, option):
response.vary = vary


_seps = {'/', os.sep}
_invalid_element_chars = {'/', os.sep, '\x00'}


def _contains_slash(item):
for sep in _seps:
if sep in item:
def _contains_invalid_element_char(item):
for invalid_element_char in _invalid_element_chars:
if invalid_element_char in item:
return True


Expand All @@ -279,7 +279,7 @@ def _secure_path(path_tuple):
# unless someone screws up the traversal_path code
# (request.subpath is computed via traversal_path too)
return None
if any([_contains_slash(item) for item in path_tuple]):
if any([_contains_invalid_element_char(item) for item in path_tuple]):
return None
encoded = '/'.join(path_tuple) # will be unicode
return encoded
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/index.html
@@ -0,0 +1 @@
<h1>DON'T GO HERE</h1>
9 changes: 9 additions & 0 deletions tests/pkgs/static_abspath_nulbyte/__init__.py
@@ -0,0 +1,9 @@
import os


def includeme(config):
here = here = os.path.dirname(__file__)
static = os.path.normpath(
os.path.join(here, '..', '..', 'fixtures', 'static')
)
config.add_static_view('/', static)
6 changes: 6 additions & 0 deletions tests/pkgs/static_assetspec_nulbyte/__init__.py
@@ -0,0 +1,6 @@
def includeme(config):
config.add_static_view('/', 'tests:fixtures/static')
config.add_static_view('/sub', 'tests:fixtures/static/subdir')
config.override_asset(
'tests:fixtures/static/subdir', 'tests:fixtures/static'
)
20 changes: 20 additions & 0 deletions tests/test_integration.py
Expand Up @@ -225,6 +225,26 @@ class TestStaticAppUsingAssetSpec(StaticAppBase, unittest.TestCase):
package = 'tests.pkgs.static_assetspec'


class TestStaticAppUsingAbsPathNulByte(IntegrationBase, unittest.TestCase):
package = 'tests.pkgs.static_abspath_nulbyte'

def test_nulbyte_chroot(self):
super_w_null = '..\x00/'
self.testapp.get(f'/{super_w_null}', status=404)


class TestStaticAppUsingAssetSpecNulByte(IntegrationBase, unittest.TestCase):
package = 'tests.pkgs.static_assetspec_nulbyte'

def test_nulbyte_chroot(self):
super_w_null = '..\x00/'
self.testapp.get(f'/{super_w_null}', status=404)

def test_nulbyte_chroot_assetspec_override(self):
super_w_null = '..\x00/'
self.testapp.get(f'/sub/{super_w_null}', status=404)


class TestStaticAppWithEncodings(IntegrationBase, unittest.TestCase):
package = 'tests.pkgs.static_encodings'

Expand Down
11 changes: 11 additions & 0 deletions tests/test_static.py
Expand Up @@ -104,6 +104,17 @@ def test_oob_os_sep(self):

self.assertRaises(HTTPNotFound, inst, context, request)

def test_oob_nul_char(self):
import os

inst = self._makeOne(f'{os.getcwd()}/tests/fixtures/static')
super_w_null = '..\x00/'
request = self._makeRequest({'PATH_INFO': f'/{super_w_null}'})
context = DummyContext()
from pyramid.httpexceptions import HTTPNotFound

self.assertRaises(HTTPNotFound, inst, context, request)

def test_resource_doesnt_exist(self):
inst = self._makeOne('tests:fixtures/static')
request = self._makeRequest({'PATH_INFO': '/notthere'})
Expand Down

0 comments on commit b4e78bd

Please sign in to comment.