diff --git a/docs/changelog.md b/docs/changelog.md index 189c74ce1..29f955256 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,9 +1,44 @@ # Changelog +## paperless-ngx 2.20.4 + +### Security + +- Resolve [GHSA-28cf-xvcf-hw6m](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-28cf-xvcf-hw6m) + +### Bug Fixes + +- Fix: propagate metadata override created value [@shamoon](https://github.com/shamoon) ([#11659](https://github.com/paperless-ngx/paperless-ngx/pull/11659)) +- Fix: support ordering by storage path name [@shamoon](https://github.com/shamoon) ([#11661](https://github.com/paperless-ngx/paperless-ngx/pull/11661)) +- Fix: validate cf integer values within PostgreSQL range [@shamoon](https://github.com/shamoon) ([#11666](https://github.com/paperless-ngx/paperless-ngx/pull/11666)) +- Fixhancement: add error handling and retry when opening index [@shamoon](https://github.com/shamoon) ([#11731](https://github.com/paperless-ngx/paperless-ngx/pull/11731)) +- Fix: fix recurring workflow to respect latest run time [@shamoon](https://github.com/shamoon) ([#11735](https://github.com/paperless-ngx/paperless-ngx/pull/11735)) + +### All App Changes + +
+5 changes + +- Fix: propagate metadata override created value [@shamoon](https://github.com/shamoon) ([#11659](https://github.com/paperless-ngx/paperless-ngx/pull/11659)) +- Fix: support ordering by storage path name [@shamoon](https://github.com/shamoon) ([#11661](https://github.com/paperless-ngx/paperless-ngx/pull/11661)) +- Fix: validate cf integer values within PostgreSQL range [@shamoon](https://github.com/shamoon) ([#11666](https://github.com/paperless-ngx/paperless-ngx/pull/11666)) +- Fixhancement: add error handling and retry when opening index [@shamoon](https://github.com/shamoon) ([#11731](https://github.com/paperless-ngx/paperless-ngx/pull/11731)) +- Fix: fix recurring workflow to respect latest run time [@shamoon](https://github.com/shamoon) ([#11735](https://github.com/paperless-ngx/paperless-ngx/pull/11735)) +
+ ## paperless-ngx 2.20.3 +### Security + +- Resolve [GHSA-7cq3-mhxq-w946](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-7cq3-mhxq-w946) + ## paperless-ngx 2.20.2 +### Security + +- Resolve [GHSA-6653-vcx4-69mc](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-6653-vcx4-69mc) +- Resolve [GHSA-24x5-wp64-9fcc](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-24x5-wp64-9fcc) + ### Features / Enhancements - Tweakhancement: dim inactive users in users-groups list [@shamoon](https://github.com/shamoon) ([#11537](https://github.com/paperless-ngx/paperless-ngx/pull/11537)) diff --git a/docs/configuration.md b/docs/configuration.md index 2517d9cc1..6a3624d36 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -170,11 +170,18 @@ Available options are `postgresql` and `mariadb`. !!! note - A small pool is typically sufficient — for example, a size of 4. - Make sure your PostgreSQL server's max_connections setting is large enough to handle: - ```(Paperless workers + Celery workers) × pool size + safety margin``` - For example, with 4 Paperless workers and 2 Celery workers, and a pool size of 4: - (4 + 2) × 4 + 10 = 34 connections required. + A pool of 8-10 connections per worker is typically sufficient. + If you encounter error messages such as `couldn't get a connection` + or database connection timeouts, you probably need to increase the pool size. + + !!! warning + Make sure your PostgreSQL `max_connections` setting is large enough to handle the connection pools: + `(NB_PAPERLESS_WORKERS + NB_CELERY_WORKERS) × POOL_SIZE + SAFETY_MARGIN`. For example, with + 4 Paperless workers and 2 Celery workers, and a pool size of 8:``(4 + 2) × 8 + 10 = 58`, + so `max_connections = 60` (or even more) is appropriate. + + This assumes only Paperless-ngx connects to your PostgreSQL instance. If you have other applications, + you should increase `max_connections` accordingly. #### [`PAPERLESS_DB_READ_CACHE_ENABLED=`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED} diff --git a/pyproject.toml b/pyproject.toml index 2ba8325b3..a030ad840 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "paperless-ngx" -version = "2.20.3" +version = "2.20.4" description = "A community-supported supercharged document management system: scan, index and archive all your physical documents" readme = "README.md" requires-python = ">=3.10" diff --git a/src-ui/package.json b/src-ui/package.json index 7b5954b53..d7c53fc46 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -1,6 +1,6 @@ { "name": "paperless-ngx-ui", - "version": "2.20.3", + "version": "2.20.4", "scripts": { "preinstall": "npx only-allow pnpm", "ng": "ng", diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index c8bb844e9..d27ab9966 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -6,7 +6,7 @@ export const environment = { apiVersion: '9', // match src/paperless/settings.py appTitle: 'Paperless-ngx', tag: 'prod', - version: '2.20.3', + version: '2.20.4', webSocketHost: window.location.host, webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', webSocketBaseUrl: base_url.pathname + 'ws/', diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index c06ffb641..bff68780b 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -421,7 +421,15 @@ def update_filename_and_move_files( return instance = instance.document - def validate_move(instance, old_path: Path, new_path: Path): + def validate_move(instance, old_path: Path, new_path: Path, root: Path): + if not new_path.is_relative_to(root): + msg = ( + f"Document {instance!s}: Refusing to move file outside root {root}: " + f"{new_path}." + ) + logger.warning(msg) + raise CannotMoveFilesException(msg) + if not old_path.is_file(): # Can't do anything if the old file does not exist anymore. msg = f"Document {instance!s}: File {old_path} doesn't exist." @@ -510,12 +518,22 @@ def update_filename_and_move_files( return if move_original: - validate_move(instance, old_source_path, instance.source_path) + validate_move( + instance, + old_source_path, + instance.source_path, + settings.ORIGINALS_DIR, + ) create_source_path_directory(instance.source_path) shutil.move(old_source_path, instance.source_path) if move_archive: - validate_move(instance, old_archive_path, instance.archive_path) + validate_move( + instance, + old_archive_path, + instance.archive_path, + settings.ARCHIVE_DIR, + ) create_source_path_directory(instance.archive_path) shutil.move(old_archive_path, instance.archive_path) diff --git a/src/documents/templating/filepath.py b/src/documents/templating/filepath.py index 7d76e7f31..805cefbdb 100644 --- a/src/documents/templating/filepath.py +++ b/src/documents/templating/filepath.py @@ -262,6 +262,17 @@ def get_custom_fields_context( return field_data +def _is_safe_relative_path(value: str) -> bool: + if value == "": + return True + + path = PurePath(value) + if path.is_absolute() or path.drive: + return False + + return ".." not in path.parts + + def validate_filepath_template_and_render( template_string: str, document: Document | None = None, @@ -309,6 +320,12 @@ def validate_filepath_template_and_render( ) rendered_template = template.render(context) + if not _is_safe_relative_path(rendered_template): + logger.warning( + "Template rendered an unsafe path (absolute or containing traversal).", + ) + return None + # We're good! return rendered_template except UndefinedError: diff --git a/src/documents/tests/test_api_objects.py b/src/documents/tests/test_api_objects.py index 014dd3c2a..0eb99f023 100644 --- a/src/documents/tests/test_api_objects.py +++ b/src/documents/tests/test_api_objects.py @@ -219,6 +219,30 @@ class TestApiStoragePaths(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(StoragePath.objects.count(), 1) + def test_api_create_storage_path_rejects_traversal(self): + """ + GIVEN: + - API request to create a storage paths + - Storage path attempts directory traversal + WHEN: + - API is called + THEN: + - Correct HTTP 400 response + - No storage path is created + """ + response = self.client.post( + self.ENDPOINT, + json.dumps( + { + "name": "Traversal path", + "path": "../../../../../tmp/proof", + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(StoragePath.objects.count(), 1) + def test_api_storage_path_placeholders(self): """ GIVEN: diff --git a/src/paperless/version.py b/src/paperless/version.py index c0c6439d4..0ce227357 100644 --- a/src/paperless/version.py +++ b/src/paperless/version.py @@ -1,6 +1,6 @@ from typing import Final -__version__: Final[tuple[int, int, int]] = (2, 20, 3) +__version__: Final[tuple[int, int, int]] = (2, 20, 4) # Version string like X.Y.Z __full_version_str__: Final[str] = ".".join(map(str, __version__)) # Version string like X.Y diff --git a/uv.lock b/uv.lock index 1a6b6b1d7..596bebc75 100644 --- a/uv.lock +++ b/uv.lock @@ -2961,7 +2961,7 @@ wheels = [ [[package]] name = "paperless-ngx" -version = "2.20.3" +version = "2.20.4" source = { virtual = "." } dependencies = [ { name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },