Skip to content

Commit

Permalink
Merge branch 'backport-jp_exploit_fix' into 2.0-branch
Browse files Browse the repository at this point in the history
  • Loading branch information
mmerickel committed Aug 25, 2023
2 parents 9cd83a2 + 8dc51af commit 1352ca8
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 7 deletions.
25 changes: 25 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
.. _changes_2.0.2:

2.0.2 (2023-08-24)
==========

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.

.. _changes_2.0.1:

2.0.1 (2023-01-29)
Expand Down
1 change: 1 addition & 0 deletions docs/whatsnew-2.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Pyramid 2.0 was released on 2021-02-28.
The following bug fix releases were made since then. Bug fix releases also include documentation improvements and other minor feature changes.

- :ref:`changes_2.0.1`
- :ref:`changes_2.0.2`

Feature Additions
-----------------
Expand Down
14 changes: 7 additions & 7 deletions src/pyramid/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,11 @@ def get_resource_name(self, request):

# normalize asset spec or fs path into resource_path
if self.package_name: # package resource
resource_path = '%s/%s' % (self.docroot.rstrip('/'), path)
resource_path = '{}/{}'.format(self.docroot.rstrip('/'), path)
if resource_isdir(self.package_name, resource_path):
if not request.path_url.endswith('/'):
raise self.add_slash_redirect(request)
resource_path = '%s/%s' % (
resource_path = '{}/{}'.format(
resource_path.rstrip('/'),
self.index,
)
Expand Down 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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<h1>DON'T GO HERE</h1>
9 changes: 9 additions & 0 deletions tests/pkgs/static_abspath_nulbyte/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 1352ca8

Please sign in to comment.