Skip to content

Commit

Permalink
Fix BLP DOS -- CVE-2021-28678
Browse files Browse the repository at this point in the history
* BlpImagePlugin did not properly check that reads after jumping to
  file offsets returned data. This could lead to a DOS where the
  decoder could be run a large number of times on empty data
* This dates to Pillow 5.1.0
  • Loading branch information
wiredfool authored and hugovk committed Apr 1, 2021
1 parent 22e9bee commit 496245a
Show file tree
Hide file tree
Showing 9 changed files with 42 additions and 20 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
19 changes: 19 additions & 0 deletions Tests/test_file_blp.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,22 @@ def test_load_blp2_dxt1():
def test_load_blp2_dxt1a():
with Image.open("Tests/images/blp/blp2_dxt1a.blp") as im:
assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png")


@pytest.mark.parametrize(
"test_file",
[
"Tests/images/timeout-060745d3f534ad6e4128c51d336ea5489182c69d.blp",
"Tests/images/timeout-31c8f86233ea728339c6e586be7af661a09b5b98.blp",
"Tests/images/timeout-60d8b7c8469d59fc9ffff6b3a3dc0faeae6ea8ee.blp",
"Tests/images/timeout-8073b430977660cdd48d96f6406ddfd4114e69c7.blp",
"Tests/images/timeout-bba4f2e026b5786529370e5dfe9a11b1bf991f07.blp",
"Tests/images/timeout-d6ec061c4afdef39d3edf6da8927240bb07fe9b7.blp",
"Tests/images/timeout-ef9112a065e7183fa7faa2e18929b03e44ee16bf.blp",
],
)
def test_crashes(test_file):
with open(test_file, "rb") as f:
with Image.open(f) as im:
with pytest.raises(OSError):
im.load()
43 changes: 23 additions & 20 deletions src/PIL/BlpImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,33 +286,36 @@ def decode(self, buffer):
raise OSError("Truncated Blp file") from e
return 0, 0

def _safe_read(self, length):
return ImageFile._safe_read(self.fd, length)

def _read_palette(self):
ret = []
for i in range(256):
try:
b, g, r, a = struct.unpack("<4B", self.fd.read(4))
b, g, r, a = struct.unpack("<4B", self._safe_read(4))
except struct.error:
break
ret.append((b, g, r, a))
return ret

def _read_blp_header(self):
(self._blp_compression,) = struct.unpack("<i", self.fd.read(4))
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))

(self._blp_encoding,) = struct.unpack("<b", self.fd.read(1))
(self._blp_alpha_depth,) = struct.unpack("<b", self.fd.read(1))
(self._blp_alpha_encoding,) = struct.unpack("<b", self.fd.read(1))
(self._blp_mips,) = struct.unpack("<b", self.fd.read(1))
(self._blp_encoding,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_depth,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_encoding,) = struct.unpack("<b", self._safe_read(1))
(self._blp_mips,) = struct.unpack("<b", self._safe_read(1))

self.size = struct.unpack("<II", self.fd.read(8))
self.size = struct.unpack("<II", self._safe_read(8))

if self.magic == b"BLP1":
# Only present for BLP1
(self._blp_encoding,) = struct.unpack("<i", self.fd.read(4))
(self._blp_subtype,) = struct.unpack("<i", self.fd.read(4))
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4))
(self._blp_subtype,) = struct.unpack("<i", self._safe_read(4))

self._blp_offsets = struct.unpack("<16I", self.fd.read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self.fd.read(16 * 4))
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))


class BLP1Decoder(_BLPBaseDecoder):
Expand All @@ -324,7 +327,7 @@ def _load(self):
if self._blp_encoding in (4, 5):
data = bytearray()
palette = self._read_palette()
_data = BytesIO(self.fd.read(self._blp_lengths[0]))
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
Expand All @@ -346,10 +349,10 @@ def _load(self):
def _decode_jpeg_stream(self):
from PIL.JpegImagePlugin import JpegImageFile

(jpeg_header_size,) = struct.unpack("<I", self.fd.read(4))
jpeg_header = self.fd.read(jpeg_header_size)
self.fd.read(self._blp_offsets[0] - self.fd.tell()) # What IS this?
data = self.fd.read(self._blp_lengths[0])
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
jpeg_header = self._safe_read(jpeg_header_size)
self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this?
data = self._safe_read(self._blp_lengths[0])
data = jpeg_header + data
data = BytesIO(data)
image = JpegImageFile(data)
Expand All @@ -370,7 +373,7 @@ def _load(self):
# Uncompressed or DirectX compression

if self._blp_encoding == BLP_ENCODING_UNCOMPRESSED:
_data = BytesIO(self.fd.read(self._blp_lengths[0]))
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
Expand All @@ -384,20 +387,20 @@ def _load(self):
linesize = (self.size[0] + 3) // 4 * 8
for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt1(
self.fd.read(linesize), alpha=bool(self._blp_alpha_depth)
self._safe_read(linesize), alpha=bool(self._blp_alpha_depth)
):
data += d

elif self._blp_alpha_encoding == BLP_ALPHA_ENCODING_DXT3:
linesize = (self.size[0] + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt3(self.fd.read(linesize)):
for d in decode_dxt3(self._safe_read(linesize)):
data += d

elif self._blp_alpha_encoding == BLP_ALPHA_ENCODING_DXT5:
linesize = (self.size[0] + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt5(self.fd.read(linesize)):
for d in decode_dxt5(self._safe_read(linesize)):
data += d
else:
raise BLPFormatError(
Expand Down

0 comments on commit 496245a

Please sign in to comment.