From 347d7750da6f45c7436dd0c31468885cc9343c85 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 21 Aug 2023 14:43:12 -0400 Subject: [PATCH 1/8] fix: reject NUL character as path element See: https://github.com/Pylons/pyramid/security/advisories/GHSA-j8g2-6fc7-q8f8 --- src/pyramid/static.py | 10 +++++----- tests/fixtures/index.html | 1 + tests/test_static.py | 13 +++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/index.html diff --git a/src/pyramid/static.py b/src/pyramid/static.py index 5363c16716..e2a5e68d33 100644 --- a/src/pyramid/static.py +++ b/src/pyramid/static.py @@ -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 @@ -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 diff --git a/tests/fixtures/index.html b/tests/fixtures/index.html new file mode 100644 index 0000000000..a37df5790d --- /dev/null +++ b/tests/fixtures/index.html @@ -0,0 +1 @@ +

DON'T GO HERE

diff --git a/tests/test_static.py b/tests/test_static.py index 3fc6586e9f..29de0b8702 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -104,6 +104,19 @@ 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') + dds = '..\x00/' + request = self._makeRequest( + {'PATH_INFO': f'/{dds}'} + ) + 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'}) From d6ac1ed260b156109f524dc95e3f9f369752a36b Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 21 Aug 2023 14:50:34 -0400 Subject: [PATCH 2/8] chore: appease lint, better varname --- tests/test_static.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_static.py b/tests/test_static.py index 29de0b8702..2ed79105db 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -108,10 +108,8 @@ def test_oob_nul_char(self): import os inst = self._makeOne(f'{os.getcwd()}/tests/fixtures/static') - dds = '..\x00/' - request = self._makeRequest( - {'PATH_INFO': f'/{dds}'} - ) + super_w_null = '..\x00/' + request = self._makeRequest({'PATH_INFO': f'/{super_w_null}'}) context = DummyContext() from pyramid.httpexceptions import HTTPNotFound From 26182b6bd3708bded302e2bf157e6049b5476582 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 21 Aug 2023 14:53:38 -0400 Subject: [PATCH 3/8] chore: back out unintended change --- src/pyramid/static.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pyramid/static.py b/src/pyramid/static.py index e2a5e68d33..100d17676e 100644 --- a/src/pyramid/static.py +++ b/src/pyramid/static.py @@ -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, ) From 2b74cc2ae8016d5e3d79a8d3eae665ceb4a65fd2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 21 Aug 2023 18:06:29 -0400 Subject: [PATCH 4/8] add integration tests --- tests/pkgs/static_abspath_nulbyte/__init__.py | 10 ++++++++++ tests/pkgs/static_assetspec_nulbyte/__init__.py | 2 ++ tests/test_integration.py | 7 +++++++ 3 files changed, 19 insertions(+) create mode 100644 tests/pkgs/static_abspath_nulbyte/__init__.py create mode 100644 tests/pkgs/static_assetspec_nulbyte/__init__.py diff --git a/tests/pkgs/static_abspath_nulbyte/__init__.py b/tests/pkgs/static_abspath_nulbyte/__init__.py new file mode 100644 index 0000000000..a946fcdf98 --- /dev/null +++ b/tests/pkgs/static_abspath_nulbyte/__init__.py @@ -0,0 +1,10 @@ +import os + + +def includeme(config): + here = here = os.path.dirname(__file__) + static + static = os.path.normpath( + os.path.join(here, '..', '..', 'fixtures', 'statc') + ) + config.add_static_view('/', static) diff --git a/tests/pkgs/static_assetspec_nulbyte/__init__.py b/tests/pkgs/static_assetspec_nulbyte/__init__.py new file mode 100644 index 0000000000..49cebab3e0 --- /dev/null +++ b/tests/pkgs/static_assetspec_nulbyte/__init__.py @@ -0,0 +1,2 @@ +def includeme(config): + config.add_static_view('/', 'tests:fixtures/static') diff --git a/tests/test_integration.py b/tests/test_integration.py index f671b7c0b9..a7d9e9b6c3 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -220,10 +220,17 @@ def test_sendfoobar(self): class TestStaticAppUsingAbsPath(StaticAppBase, unittest.TestCase): package = 'tests.pkgs.static_abspath' + def test_nulbyte_chroot(self): + super_w_null = '/static/..\x00/' + res = self.testapp.get(f'/{super_w_null}', status=404) + class TestStaticAppUsingAssetSpec(StaticAppBase, unittest.TestCase): package = 'tests.pkgs.static_assetspec' + def test_nulbyte_chroot(self): + super_w_null = 'static/..\x00/' + res = self.testapp.get(f'/{super_w_null}', status=404) class TestStaticAppWithEncodings(IntegrationBase, unittest.TestCase): package = 'tests.pkgs.static_encodings' From 6623dfc84612b3a509e71438808fc3b4270d6c00 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 21 Aug 2023 18:27:04 -0400 Subject: [PATCH 5/8] re-add integration tests (bad merge) and add integration test for nulbyte check when asset spec override exists --- tests/pkgs/static_abspath_nulbyte/__init__.py | 3 +-- .../pkgs/static_assetspec_nulbyte/__init__.py | 3 +++ tests/test_integration.py | 22 ++++++++++++++----- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/pkgs/static_abspath_nulbyte/__init__.py b/tests/pkgs/static_abspath_nulbyte/__init__.py index a946fcdf98..2248522e95 100644 --- a/tests/pkgs/static_abspath_nulbyte/__init__.py +++ b/tests/pkgs/static_abspath_nulbyte/__init__.py @@ -3,8 +3,7 @@ def includeme(config): here = here = os.path.dirname(__file__) - static static = os.path.normpath( - os.path.join(here, '..', '..', 'fixtures', 'statc') + os.path.join(here, '..', '..', 'fixtures', 'static') ) config.add_static_view('/', static) diff --git a/tests/pkgs/static_assetspec_nulbyte/__init__.py b/tests/pkgs/static_assetspec_nulbyte/__init__.py index 49cebab3e0..5ac6e8cf59 100644 --- a/tests/pkgs/static_assetspec_nulbyte/__init__.py +++ b/tests/pkgs/static_assetspec_nulbyte/__init__.py @@ -1,2 +1,5 @@ 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') diff --git a/tests/test_integration.py b/tests/test_integration.py index a7d9e9b6c3..18aa44855f 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -220,18 +220,30 @@ def test_sendfoobar(self): class TestStaticAppUsingAbsPath(StaticAppBase, unittest.TestCase): package = 'tests.pkgs.static_abspath' - def test_nulbyte_chroot(self): - super_w_null = '/static/..\x00/' - res = self.testapp.get(f'/{super_w_null}', status=404) - 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 = 'static/..\x00/' + super_w_null = '..\x00/' res = 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/' + res = self.testapp.get(f'/{super_w_null}', status=404) + + def test_nulbyte_chroot_assetspec_override(self): + super_w_null = '..\x00/' + res = self.testapp.get(f'/sub/{super_w_null}', status=404) + + class TestStaticAppWithEncodings(IntegrationBase, unittest.TestCase): package = 'tests.pkgs.static_encodings' From aca53377aaaf515ef803f5f1c6fb359b6049aa0b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 21 Aug 2023 18:43:33 -0400 Subject: [PATCH 6/8] appease linter --- tests/test_integration.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 18aa44855f..7735b712bc 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -230,18 +230,19 @@ class TestStaticAppUsingAbsPathNulByte(IntegrationBase, unittest.TestCase): def test_nulbyte_chroot(self): super_w_null = '..\x00/' - res = self.testapp.get(f'/{super_w_null}', status=404) + 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/' - res = self.testapp.get(f'/{super_w_null}', status=404) + self.testapp.get(f'/{super_w_null}', status=404) def test_nulbyte_chroot_assetspec_override(self): super_w_null = '..\x00/' - res = self.testapp.get(f'/sub/{super_w_null}', status=404) + self.testapp.get(f'/sub/{super_w_null}', status=404) class TestStaticAppWithEncodings(IntegrationBase, unittest.TestCase): From 538a70688f946db3227b2e4db9b3a2e64921c57e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 21 Aug 2023 18:44:48 -0400 Subject: [PATCH 7/8] appease linter --- tests/pkgs/static_assetspec_nulbyte/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/pkgs/static_assetspec_nulbyte/__init__.py b/tests/pkgs/static_assetspec_nulbyte/__init__.py index 5ac6e8cf59..d44b04e932 100644 --- a/tests/pkgs/static_assetspec_nulbyte/__init__.py +++ b/tests/pkgs/static_assetspec_nulbyte/__init__.py @@ -1,5 +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') + config.override_asset( + 'tests:fixtures/static/subdir', 'tests:fixtures/static' + ) From 8dc51af74fbd266c84139bf43662cd2d373fd769 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 24 Aug 2023 23:47:19 -0600 Subject: [PATCH 8/8] update changelog for 2.0.2 --- CHANGES.rst | 25 +++++++++++++++++++++++++ docs/whatsnew-2.0.rst | 1 + 2 files changed, 26 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e83c0e7d17..8dbf89b6f5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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) diff --git a/docs/whatsnew-2.0.rst b/docs/whatsnew-2.0.rst index fa2c12df79..3b383f9f04 100644 --- a/docs/whatsnew-2.0.rst +++ b/docs/whatsnew-2.0.rst @@ -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 -----------------