Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ jobs:
- tests/mock_vws/test_update_target.py::TestImage::test_image_valid
- tests/mock_vws/test_update_target.py::TestImage::test_bad_image_format_or_color_space
- tests/mock_vws/test_update_target.py::TestImage::test_corrupted
- tests/mock_vws/test_update_target.py::TestImage::test_truncated
- tests/mock_vws/test_update_target.py::TestImage::test_image_too_large
- tests/mock_vws/test_update_target.py::TestImage::test_not_base64_encoded_processable
- tests/mock_vws/test_update_target.py::TestImage::test_not_base64_encoded_not_processable
Expand Down
1 change: 1 addition & 0 deletions newsfragments/3498.change
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Return a ``BadImage`` response, rather than failing to respond, when an image given to the Target API is truncated before the end of its image data.
8 changes: 7 additions & 1 deletion src/mock_vws/_services_validators/image_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ def validate_image_integrity(*, request_body: bytes) -> None:
with open_image(fp=image_file) as pil_image:
try:
pil_image.verify()
except SyntaxError as exc:
except (OSError, SyntaxError) as exc:
# ``verify`` raises ``SyntaxError`` for a damaged header and
# ``OSError`` for damaged image data, such as a PNG which is
# truncated before its ``IEND`` chunk.
# ``open_image`` runs outside this ``try``, so anything which
# cannot be opened at all is already rejected by
# ``validate_image_is_image``.
_LOGGER.warning(msg="The image is not a valid image file.")
raise BadImageError from exc

Expand Down
25 changes: 25 additions & 0 deletions tests/mock_vws/test_add_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from tests.mock_vws.utils import (
make_decompression_bomb_image_file,
make_single_color_image_file,
make_truncated_png_file,
)
from tests.mock_vws.utils.assertions import (
assert_vws_failure,
Expand Down Expand Up @@ -505,6 +506,30 @@ def test_corrupted(
result_code=ResultCodes.BAD_IMAGE,
)

@staticmethod
def test_truncated(vws_client: VWS) -> None:
"""
An error is returned when the given image is truncated before
the
end of its image data.
"""
image_file = make_truncated_png_file()

with pytest.raises(expected_exception=BadImageError) as exc:
vws_client.add_target(
name="example_name",
width=1,
image=image_file,
application_metadata=None,
active_flag=True,
)

assert_vws_failure(
response=exc.value.response,
status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
result_code=ResultCodes.BAD_IMAGE,
)

@staticmethod
def test_decompression_bomb(vws_client: VWS) -> None:
"""
Expand Down
26 changes: 26 additions & 0 deletions tests/mock_vws/test_update_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from vws_test_fixtures.images import VWS_MAX_IMAGE_FILE_SIZE

from mock_vws._constants import ResultCodes
from tests.mock_vws.utils import make_truncated_png_file
from tests.mock_vws.utils.assertions import (
assert_vws_failure,
assert_vws_response,
Expand Down Expand Up @@ -666,6 +667,31 @@ def test_corrupted(
result_code=ResultCodes.BAD_IMAGE,
)

@staticmethod
def test_truncated(
*,
vws_client: VWS,
target_id: str,
) -> None:
"""
An error is returned when the given image is truncated before
the
end of its image data.
"""
image_file = make_truncated_png_file()

with pytest.raises(expected_exception=BadImageError) as exc:
vws_client.update_target(
target_id=target_id,
image=image_file,
)

assert_vws_failure(
response=exc.value.response,
status_code=HTTPStatus.UNPROCESSABLE_ENTITY,
result_code=ResultCodes.BAD_IMAGE,
)

@staticmethod
def test_image_too_large(
*,
Expand Down
21 changes: 21 additions & 0 deletions tests/mock_vws/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,27 @@ def make_single_color_image_file(*, width: int, height: int) -> io.BytesIO:
return image_buffer


@beartype
def make_truncated_png_file() -> io.BytesIO:
"""Return a PNG file whose image data is cut off before the end.

The file keeps a valid signature and header, so Pillow opens it, but the
``IEND`` chunk which marks the end of the image is missing, so decoding
the image data fails.

Returns:
A PNG file which is truncated before its ``IEND`` chunk.
"""
image = Image.frombytes(mode="RGB", size=(1, 1), data=b"\x01\x02\x03")
image_buffer = io.BytesIO()
image.save(fp=image_buffer, format="PNG", optimize=True)
image_bytes = image_buffer.getvalue()
# Remove the ``IEND`` chunk: its four length bytes, its four type bytes
# and its four checksum bytes.
truncated = image_bytes[: image_bytes.rindex(b"IEND") - 4]
return io.BytesIO(initial_bytes=truncated)


@beartype
def make_decompression_bomb_image_file() -> io.BytesIO:
"""Return a PNG file which is tiny on disk but huge when decoded.
Expand Down
Loading