security: fix #8 — install MAX_IMAGE_PIXELS cap in core/__init__.py
PIL's decompression-bomb cap previously lived as a side effect of importing core/cache.py. Any future code path that touched core/images (or any other core submodule) without first importing cache would silently revert to PIL's default 89M-pixel *warning* (not an error), re-opening the bomb surface. Moves the cap into core/__init__.py so any import of any booru_viewer.core.* submodule installs it first. The duplicate set in cache.py is left in place by this commit and removed in the next one — both writes are idempotent so this commit is bisect-safe. Audit-Ref: SECURITY_AUDIT.md finding #8 Severity: Low
This commit is contained in:
parent
6ff1f726d4
commit
2bb6352141
@ -0,0 +1,19 @@
|
|||||||
|
"""booru_viewer.core package — pure-Python data + I/O layer (no Qt).
|
||||||
|
|
||||||
|
Side effect on import: install the project-wide PIL decompression-bomb
|
||||||
|
cap. PIL's default warns silently above ~89M pixels; we want a hard
|
||||||
|
fail above 256M pixels so DecompressionBombError can be caught and
|
||||||
|
treated as a download failure.
|
||||||
|
|
||||||
|
Setting it here (rather than as a side effect of importing
|
||||||
|
``core.cache``) means any code path that touches PIL via any
|
||||||
|
``booru_viewer.core.*`` submodule gets the cap installed first —
|
||||||
|
``core.images`` no longer depends on ``core.cache`` having been
|
||||||
|
imported in the right order. Audit finding #8.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PIL import Image as _PILImage
|
||||||
|
|
||||||
|
_PILImage.MAX_IMAGE_PIXELS = 256 * 1024 * 1024
|
||||||
|
|
||||||
|
del _PILImage
|
||||||
57
tests/core/test_pil_safety.py
Normal file
57
tests/core/test_pil_safety.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"""Tests for the project-wide PIL decompression-bomb cap (audit #8).
|
||||||
|
|
||||||
|
The cap lives in `booru_viewer/core/__init__.py` so any import of
|
||||||
|
any `booru_viewer.core.*` submodule installs it first — independent
|
||||||
|
of whether `core.cache` is on the import path. Both checks are run
|
||||||
|
in a fresh subprocess so the assertion isn't masked by some other
|
||||||
|
test's previous import.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
EXPECTED = 256 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def _run(code: str) -> str:
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-c", code],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def test_core_package_import_installs_cap():
|
||||||
|
"""Importing the core package alone must set MAX_IMAGE_PIXELS."""
|
||||||
|
out = _run(
|
||||||
|
"import booru_viewer.core; "
|
||||||
|
"from PIL import Image; "
|
||||||
|
"print(Image.MAX_IMAGE_PIXELS)"
|
||||||
|
)
|
||||||
|
assert int(out) == EXPECTED
|
||||||
|
|
||||||
|
|
||||||
|
def test_core_images_import_installs_cap():
|
||||||
|
"""The original audit concern: importing core.images without first
|
||||||
|
importing core.cache must still set the cap."""
|
||||||
|
out = _run(
|
||||||
|
"from booru_viewer.core import images; "
|
||||||
|
"from PIL import Image; "
|
||||||
|
"print(Image.MAX_IMAGE_PIXELS)"
|
||||||
|
)
|
||||||
|
assert int(out) == EXPECTED
|
||||||
|
|
||||||
|
|
||||||
|
def test_core_cache_import_still_installs_cap():
|
||||||
|
"""Regression: the old code path (importing cache first) must keep
|
||||||
|
working after the move."""
|
||||||
|
out = _run(
|
||||||
|
"from booru_viewer.core import cache; "
|
||||||
|
"from PIL import Image; "
|
||||||
|
"print(Image.MAX_IMAGE_PIXELS)"
|
||||||
|
)
|
||||||
|
assert int(out) == EXPECTED
|
||||||
Loading…
x
Reference in New Issue
Block a user