Repository: paperless-ngx/paperless-ngx

Issue:
[BUG] DecompressionBombError: Image size exceeds limit
**Describe the bug**
For some Images and some specific PDFs I get this error in paperless:
```
DecompressionBombError: Image size (376320000 pixels) exceeds limit of 256000000 pixels, could be decompression bomb DOS attack.
```

**To Reproduce**
Steps to reproduce the behavior:
1. Upload the specific file to paperless in any way.
2. See error in the paperless UI + logs

**Expected behavior**
The file should be parsed since this is not a decompressionbomb, but a casual image.

**Webserver logs**
```
23:04:15 [Q] INFO Enqueued 1
23:04:15 [Q] INFO Process-1:1500 processing [2000-03-04 IMG_20210309_183852.pdf]
[2022-03-16 23:04:15,822] [INFO] [paperless.consumer] Consuming 2000-03-04 IMG_20210309_183852.pdf
[2022-03-16 23:04:20,729] [ERROR] [paperless.consumer] Error while consuming document 2000-03-04 IMG_20210309_183852.pdf: DecompressionBombError: Image size (376320000 pixels) exceeds limit of 256000000 pixels, could be decompression bomb DOS attack.
Traceback (most recent call last):
  File "/usr/src/paperless/src/paperless_tesseract/parsers.py", line 241, in parse
    ocrmypdf.ocr(**args)
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/api.py", line 340, in ocr
    return run_pipeline(options=options, plugin_manager=plugin_manager, api=True)
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_sync.py", line 374, in run_pipeline
    exec_concurrent(context, executor)
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_sync.py", line 271, in exec_concurrent
    executor(
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_concurrent.py", line 82, in __call__
    self._execute(
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/builtin_plugins/concurrency.py", line 132, in _execute
    for result in results:
  File "/usr/local/lib/python3.9/multiprocessing/pool.py", line 870, in next
    raise value
  File "/usr/local/lib/python3.9/multiprocessing/pool.py", line 125, in worker
    result = (True, func(*args, **kwds))
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_sync.py", line 184, in exec_page_sync
    rasterize_preview_out = rasterize_preview(page_context.origin, page_context)
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_pipeline.py", line 338, in rasterize_preview
    page_context.plugin_manager.hook.rasterize_pdf_page(
  File "/usr/local/lib/python3.9/site-packages/pluggy/hooks.py", line 286, in __call__
    return self._hookexec(self, self.get_hookimpls(), kwargs)
  File "/usr/local/lib/python3.9/site-packages/pluggy/manager.py", line 93, in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
  File "/usr/local/lib/python3.9/site-packages/pluggy/manager.py", line 84, in <lambda>
    self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall(
  File "/usr/local/lib/python3.9/site-packages/pluggy/callers.py", line 208, in _multicall
    return outcome.get_result()
  File "/usr/local/lib/python3.9/site-packages/pluggy/callers.py", line 80, in get_result
    raise ex[1].with_traceback(ex[2])
  File "/usr/local/lib/python3.9/site-packages/pluggy/callers.py", line 187, in _multicall
    res = hook_impl.function(*args)
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/builtin_plugins/ghostscript.py", line 68, in rasterize_pdf_page
    ghostscript.rasterize_pdf(
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_exec/ghostscript.py", line 124, in rasterize_pdf
    with Image.open(BytesIO(p.stdout)) as im:
  File "/usr/local/lib/python3.9/site-packages/PIL/Image.py", line 3009, in open
    im = _open_core(fp, filename, prefix, formats)
  File "/usr/local/lib/python3.9/site-packages/PIL/Image.py", line 2996, in _open_core
    _decompression_bomb_check(im.size)
  File "/usr/local/lib/python3.9/site-packages/PIL/Image.py", line 2905, in _decompression_bomb_check
    raise DecompressionBombError(
PIL.Image.DecompressionBombError: Image size (376320000 pixels) exceeds limit of 256000000 pixels, could be decompression bomb DOS attack.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/src/paperless/src/documents/consumer.py", line 248, in try_consume_file
    document_parser.parse(self.path, mime_type, self.filename)
  File "/usr/src/paperless/src/paperless_tesseract/parsers.py", line 290, in parse
    raise ParseError(f"{e.__class__.__name__}: {str(e)}")
documents.parsers.ParseError: DecompressionBombError: Image size (376320000 pixels) exceeds limit of 256000000 pixels, could be decompression bomb DOS attack.
23:04:20 [Q] INFO Process-1:1500 stopped doing work
23:04:20 [Q] ERROR Failed [2000-03-04 IMG_20210309_183852.pdf] - 2000-03-04 IMG_20210309_183852.pdf: Error while consuming document 2000-03-04 IMG_20210309_183852.pdf: DecompressionBombError: Image size (376320000 pixels) exceeds limit of 256000000 pixels, could be decompression bomb DOS attack. : Traceback (most recent call last):
  File "/usr/src/paperless/src/paperless_tesseract/parsers.py", line 241, in parse
    ocrmypdf.ocr(**args)
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/api.py", line 340, in ocr
    return run_pipeline(options=options, plugin_manager=plugin_manager, api=True)
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_sync.py", line 374, in run_pipeline
    exec_concurrent(context, executor)
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_sync.py", line 271, in exec_concurrent
    executor(
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_concurrent.py", line 82, in __call__
    self._execute(
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/builtin_plugins/concurrency.py", line 132, in _execute
    for result in results:
  File "/usr/local/lib/python3.9/multiprocessing/pool.py", line 870, in next
    raise value
  File "/usr/local/lib/python3.9/multiprocessing/pool.py", line 125, in worker
    result = (True, func(*args, **kwds))
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_sync.py", line 184, in exec_page_sync
    rasterize_preview_out = rasterize_preview(page_context.origin, page_context)
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_pipeline.py", line 338, in rasterize_preview
    page_context.plugin_manager.hook.rasterize_pdf_page(
  File "/usr/local/lib/python3.9/site-packages/pluggy/hooks.py", line 286, in __call__
    return self._hookexec(self, self.get_hookimpls(), kwargs)
  File "/usr/local/lib/python3.9/site-packages/pluggy/manager.py", line 93, in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
  File "/usr/local/lib/python3.9/site-packages/pluggy/manager.py", line 84, in <lambda>
    self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall(
  File "/usr/local/lib/python3.9/site-packages/pluggy/callers.py", line 208, in _multicall
    return outcome.get_result()
  File "/usr/local/lib/python3.9/site-packages/pluggy/callers.py", line 80, in get_result
    raise ex[1].with_traceback(ex[2])
  File "/usr/local/lib/python3.9/site-packages/pluggy/callers.py", line 187, in _multicall
    res = hook_impl.function(*args)
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/builtin_plugins/ghostscript.py", line 68, in rasterize_pdf_page
    ghostscript.rasterize_pdf(
  File "/usr/local/lib/python3.9/site-packages/ocrmypdf/_exec/ghostscript.py", line 124, in rasterize_pdf
    with Image.open(BytesIO(p.stdout)) as im:
  File "/usr/local/lib/python3.9/site-packages/PIL/Image.py", line 3009, in open
    im = _open_core(fp, filename, prefix, formats)
  File "/usr/local/lib/python3.9/site-packages/PIL/Image.py", line 2996, in _open_core
    _decompression_bomb_check(im.size)
  File "/usr/local/lib/python3.9/site-packages/PIL/Image.py", line 2905, in _decompression_bomb_check
    raise DecompressionBombError(
PIL.Image.DecompressionBombError: Image size (376320000 pixels) exceeds limit of 256000000 pixels, could be decompression bomb DOS attack.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/asgiref/sync.py", line 288, in main_wrap
    raise exc_info[1]
  File "/usr/src/paperless/src/documents/consumer.py", line 248, in try_consume_file
    document_parser.parse(self.path, mime_type, self.filename)
  File "/usr/src/paperless/src/paperless_tesseract/parsers.py", line 290, in parse
    raise ParseError(f"{e.__class__.__name__}: {str(e)}")
documents.parsers.ParseError: DecompressionBombError: Image size (376320000 pixels) exceeds limit of 256000000 pixels, could be decompression bomb DOS attack.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.9/site-packages/django_q/cluster.py", line 432, in worker
    res = f(*task["args"], **task["kwargs"])
  File "/usr/src/paperless/src/documents/tasks.py", line 74, in consume_file
    document = Consumer().try_consume_file(
  File "/usr/src/paperless/src/documents/consumer.py", line 266, in try_consume_file
    self._fail(
  File "/usr/src/paperless/src/documents/consumer.py", line 70, in _fail
    raise ConsumerError(f"{self.filename}: {log_message or message}")
documents.consumer.ConsumerError: 2000-03-04 IMG_20210309_183852.pdf: Error while consuming document 2000-03-04 IMG_20210309_183852.pdf: DecompressionBombError: Image size (376320000 pixels) exceeds limit of 256000000 pixels, could be decompression bomb DOS attack.

23:04:20 [Q] INFO recycled worker Process-1:1500
23:04:20 [Q] INFO Process-1:1502 ready for work at 275910

```

**Relevant information**
 - Host OS of the machine running paperless: Ubuntu 20.04.3 LTS
 - Browser: firefox
 - Version:  Paperless-ng 1.5.0 (migration to ngx will happen after this is fixed)
 - Installation method: docker

**Possible mitigations**

- Disable the limit entirely: https://stackoverflow.com/questions/51152059/pillow-in-python-wont-let-me-open-image-exceeds-limit/58920124#58920124
- hardcode an increased limit (e.g. x10) that will never be reached with normal images: https://stackoverflow.com/questions/51152059/pillow-in-python-wont-let-me-open-image-exceeds-limit/51152514#51152514
- make the limit configurable as an environment variable


Discussion:
Hello @hacker-h , thanks for reporting that.

tbh I would vote for the third option, making that image configurable via an environment variable.
Will give it a try to implement the env variable option
Although we have what should be a workaround, this looks related to ocrmypdf/OCRmyPDF#827, assuming the file isn't really a super large file.

If there's ever a document you can share, the upstream bug was looking to get a sample for debugging.
> If there's ever a document you can share, the upstream bug was looking to get a sample for debugging.

From #709 https://zg.lehrplan.ch/container/ZG_DE_Gesamtausgabe.pdf (thanks @github-cli!)
As far as I understand the changelog, this should be fixed by #441. However, setting  PAPERLESS_OCR_MAX_IMAGE_PIXELS does not seem to work for me:

My config: `PAPERLESS_OCR_MAX_IMAGE_PIXELS=3000000000`

Log:
```
documents.consumer.ConsumerError: sample.pdf: Error while consuming document Elektroinstallation.pdf: DecompressionBombError: Image size (2369741176 pixels) exceeds limit of 500000000 pixels, could be decompression bomb DOS attack.
```
The log looks like the new config option doesn't seem to do anything :/
Using the sample file linked above, I do not get the error on 1.7.0.  I did need to increase the worker timeout.  You're using 1.7.0?

```
PAPERLESS_OCR_MAX_IMAGE_PIXELS: 3000000000
PAPERLESS_WORKER_TIMEOUT: 10000
```
Interestingly, it now seems to work without any further config changes. I now had to raise the worker timeout as well.
I don't understand why, but it looks like the config just wasn't recognized before. Note that the limit is still the default in the error message, not my configured custom value:

Image size (2369741176 pixels) exceeds limit of 500000000 pixels

Resolving patch:
diff --git a/src/paperless/settings.py b/src/paperless/settings.py
--- a/src/paperless/settings.py
+++ b/src/paperless/settings.py
@@ -5,6 +5,7 @@
 import os
 import re
 from typing import Final
+from typing import Optional
 from typing import Set
 from urllib.parse import urlparse
 
@@ -551,10 +552,9 @@ def default_threads_per_worker(task_workers) -> int:
     os.getenv("PAPERLESS_OCR_ROTATE_PAGES_THRESHOLD", 12.0),
 )
 
-OCR_MAX_IMAGE_PIXELS = os.environ.get(
-    "PAPERLESS_OCR_MAX_IMAGE_PIXELS",
-    256000000,
-)
+OCR_MAX_IMAGE_PIXELS: Optional[int] = None
+if os.environ.get("PAPERLESS_OCR_MAX_IMAGE_PIXELS") is not None:
+    OCR_MAX_IMAGE_PIXELS: int = int(os.environ.get("PAPERLESS_OCR_MAX_IMAGE_PIXELS"))
 
 OCR_USER_ARGS = os.getenv("PAPERLESS_OCR_USER_ARGS", "{}")
 
diff --git a/src/paperless_tesseract/parsers.py b/src/paperless_tesseract/parsers.py
--- a/src/paperless_tesseract/parsers.py
+++ b/src/paperless_tesseract/parsers.py
@@ -8,8 +8,6 @@
 from documents.parsers import ParseError
 from PIL import Image
 
-Image.MAX_IMAGE_PIXELS = settings.OCR_MAX_IMAGE_PIXELS
-
 
 class NoTextFoundException(Exception):
     pass
@@ -225,6 +223,24 @@ def construct_ocrmypdf_parameters(
                     f"they will not be used. Error: {e}",
                 )
 
+        if settings.OCR_MAX_IMAGE_PIXELS is not None:
+            # Convert pixels to mega-pixels and provide to ocrmypdf
+            max_pixels_mpixels = settings.OCR_MAX_IMAGE_PIXELS / 1_000_000.0
+            if max_pixels_mpixels > 0:
+
+                self.log(
+                    "debug",
+                    f"Calculated {max_pixels_mpixels} megapixels for OCR",
+                )
+
+                ocrmypdf_args["max_image_mpixels"] = max_pixels_mpixels
+            else:
+                self.log(
+                    "warning",
+                    "There is an issue with PAPERLESS_OCR_MAX_IMAGE_PIXELS, "
+                    "this value must be at least 1 megapixel if set",
+                )
+
         return ocrmypdf_args
 
     def parse(self, document_path, mime_type, file_name=None):
diff --git a/src/paperless_text/parsers.py b/src/paperless_text/parsers.py
--- a/src/paperless_text/parsers.py
+++ b/src/paperless_text/parsers.py
@@ -6,8 +6,6 @@
 from PIL import ImageDraw
 from PIL import ImageFont
 
-Image.MAX_IMAGE_PIXELS = settings.OCR_MAX_IMAGE_PIXELS
-
 
 class TextDocumentParser(DocumentParser):
     """
@@ -28,7 +26,7 @@ def read_text():
         font = ImageFont.truetype(
             font=settings.THUMBNAIL_FONT_NAME,
             size=20,
-            layout_engine=ImageFont.LAYOUT_BASIC,
+            layout_engine=ImageFont.Layout.BASIC,
         )
         draw.text((5, 5), read_text(), font=font, fill="black")
 


Test patch: