diff --git a/OfficeIMO.Pdf/Reading/Filters/FlateDecoder.cs b/OfficeIMO.Pdf/Reading/Filters/FlateDecoder.cs index 8689c0989..d0a1d964f 100644 --- a/OfficeIMO.Pdf/Reading/Filters/FlateDecoder.cs +++ b/OfficeIMO.Pdf/Reading/Filters/FlateDecoder.cs @@ -1,3 +1,5 @@ +using System; +using System.IO; using System.IO.Compression; namespace OfficeIMO.Pdf.Filters; @@ -6,44 +8,92 @@ internal static class FlateDecoder { public static byte[] Decode(byte[] data) { // Try zlib (RFC1950) first when available in this target #if NET6_0_OR_GREATER - if (TryZlib(data, out var result)) return result!; + if (TryZlib(data, maxOutputBytes: null, out var result)) return result!; #endif // Try raw Deflate - if (TryInflate(data, out var result2)) return result2!; + if (TryInflate(data, maxOutputBytes: null, out var result2)) return result2!; // Try skip zlib header (2 bytes) with raw Deflate if (data.Length > 2 && IsLikelyZlib(data)) { var sliced = new byte[data.Length - 2]; Buffer.BlockCopy(data, 2, sliced, 0, sliced.Length); - if (TryInflate(sliced, out var result3)) return result3!; + if (TryInflate(sliced, maxOutputBytes: null, out var result3)) return result3!; } // Fallback to original return data; } - private static bool TryInflate(byte[] input, out byte[]? output) { + public static bool TryDecode(byte[] data, int maxOutputBytes, out byte[] output) { + if (maxOutputBytes < 0) { + output = Array.Empty(); + return false; + } + +#if NET6_0_OR_GREATER + if (TryZlib(data, maxOutputBytes, out var result)) { + output = result!; + return true; + } +#endif + + if (TryInflate(data, maxOutputBytes, out var result2)) { + output = result2!; + return true; + } + + if (data.Length > 2 && IsLikelyZlib(data)) { + var sliced = new byte[data.Length - 2]; + Buffer.BlockCopy(data, 2, sliced, 0, sliced.Length); + if (TryInflate(sliced, maxOutputBytes, out var result3)) { + output = result3!; + return true; + } + } + + if (data.Length <= maxOutputBytes) { + output = data; + return true; + } + + output = Array.Empty(); + return false; + } + + private static bool TryInflate(byte[] input, int? maxOutputBytes, out byte[]? output) { try { using var msIn = new MemoryStream(input); using var ds = new DeflateStream(msIn, CompressionMode.Decompress, leaveOpen: true); - using var msOut = new MemoryStream(); - ds.CopyTo(msOut); - output = msOut.ToArray(); - return true; + return TryCopyToByteArray(ds, maxOutputBytes, out output); } catch { output = null; return false; } } #if NET6_0_OR_GREATER - private static bool TryZlib(byte[] input, out byte[]? output) { + private static bool TryZlib(byte[] input, int? maxOutputBytes, out byte[]? output) { try { using var msIn = new MemoryStream(input); using var zs = new ZLibStream(msIn, CompressionMode.Decompress, leaveOpen: true); - using var msOut = new MemoryStream(); - zs.CopyTo(msOut); - output = msOut.ToArray(); - return true; + return TryCopyToByteArray(zs, maxOutputBytes, out output); } catch { output = null; return false; } } #endif + private static bool TryCopyToByteArray(Stream source, int? maxOutputBytes, out byte[]? output) { + using var msOut = new MemoryStream(); + var buffer = new byte[81920]; + int read; + while ((read = source.Read(buffer, 0, buffer.Length)) > 0) { + if (maxOutputBytes.HasValue && msOut.Length + read > maxOutputBytes.Value) { + output = null; + return false; + } + + msOut.Write(buffer, 0, read); + } + + output = msOut.ToArray(); + return true; + } + + private static bool IsLikelyZlib(byte[] d) { // RFC1950: first byte CMF low 4 bits = 8 for deflate; checksum of first two bytes mod 31 == 0 if (d.Length < 2) return false; diff --git a/OfficeIMO.Pdf/Rendering/Writer/PdfWriter.Images.Interlace.cs b/OfficeIMO.Pdf/Rendering/Writer/PdfWriter.Images.Interlace.cs index e873bd86a..7e4abba9e 100644 --- a/OfficeIMO.Pdf/Rendering/Writer/PdfWriter.Images.Interlace.cs +++ b/OfficeIMO.Pdf/Rendering/Writer/PdfWriter.Images.Interlace.cs @@ -1,3 +1,5 @@ +using System; +using System.IO; using OfficeIMO.Pdf.Filters; namespace OfficeIMO.Pdf; @@ -20,9 +22,14 @@ private static bool TryNormalizeAdam7PngData( } int bitsPerPixel = channels * bitDepth; - int fullRowBytes = GetPngRowByteCount(width, bitsPerPixel); - var fullRows = new byte[fullRowBytes * height]; - byte[] decoded = FlateDecoder.Decode(compressedData); + if (!TryGetPngRowByteCount(width, bitsPerPixel, out int fullRowBytes) || + !TryGetPngCheckedLength(fullRowBytes, height, 1, includeFilterByte: false, out int fullRowsLength) || + !TryDecodePngData(compressedData, out byte[] decoded, out unsupportedReason)) { + unsupportedReason ??= "PNG dimensions exceed supported limits."; + return false; + } + + var fullRows = new byte[fullRowsLength]; int offset = 0; for (int pass = 0; pass < Adam7Passes.Length; pass++) { @@ -33,8 +40,11 @@ private static bool TryNormalizeAdam7PngData( continue; } - int passRowBytes = GetPngRowByteCount(passWidth, bitsPerPixel); - int passScanlineBytes = (passRowBytes + 1) * passHeight; + if (!TryGetPngRowByteCount(passWidth, bitsPerPixel, out int passRowBytes) || + !TryGetPngScanlineLength(passRowBytes, passHeight, out int passScanlineBytes)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } if (offset + passScanlineBytes > decoded.Length) { unsupportedReason = "PNG image data ended before all interlaced scanlines were decoded."; return false; @@ -54,7 +64,12 @@ private static bool TryNormalizeAdam7PngData( CopyAdam7PassPixels(passPixels, fullRows, width, bitDepth, bitsPerPixel, passWidth, passHeight, adam7Pass); } - var normalizedRows = new byte[(fullRowBytes + 1) * height]; + if (!TryGetPngScanlineLength(fullRowBytes, height, out int normalizedRowsLength)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } + + var normalizedRows = new byte[normalizedRowsLength]; for (int row = 0; row < height; row++) { int sourceRow = row * fullRowBytes; int targetRow = row * (fullRowBytes + 1); @@ -75,8 +90,10 @@ private static void CopyAdam7PassPixels( int passWidth, int passHeight, Adam7Pass pass) { - int passRowBytes = GetPngRowByteCount(passWidth, bitsPerPixel); - int fullRowBytes = GetPngRowByteCount(width, bitsPerPixel); + if (!TryGetPngRowByteCount(passWidth, bitsPerPixel, out int passRowBytes) || + !TryGetPngRowByteCount(width, bitsPerPixel, out int fullRowBytes)) { + return; + } if (bitDepth < 8) { for (int y = 0; y < passHeight; y++) { int targetY = pass.YStart + y * pass.YStep; @@ -132,8 +149,17 @@ private static int CountAdam7Samples(int length, int start, int step) { return ((length - start - 1) / step) + 1; } - private static int GetPngRowByteCount(int pixelCount, int bitsPerPixel) => - (pixelCount * bitsPerPixel + 7) / 8; + private static bool TryGetPngRowByteCount(int pixelCount, int bitsPerPixel, out int rowBytes) { + rowBytes = 0; + long bits = (long)pixelCount * bitsPerPixel; + long bytes = (bits + 7L) / 8L; + if (bytes > int.MaxValue || bytes > MaxPngExpandedBytes) { + return false; + } + + rowBytes = (int)bytes; + return true; + } private static void WritePackedPngSample(byte[] packedRows, int rowStart, int pixelIndex, int bitDepth, int sample) { int samplesPerByte = 8 / bitDepth; diff --git a/OfficeIMO.Pdf/Rendering/Writer/PdfWriter.Images.Png16.cs b/OfficeIMO.Pdf/Rendering/Writer/PdfWriter.Images.Png16.cs index 101a1d81a..60ddcb375 100644 --- a/OfficeIMO.Pdf/Rendering/Writer/PdfWriter.Images.Png16.cs +++ b/OfficeIMO.Pdf/Rendering/Writer/PdfWriter.Images.Png16.cs @@ -1,3 +1,5 @@ +using System; +using System.IO; using OfficeIMO.Pdf.Filters; namespace OfficeIMO.Pdf; @@ -51,10 +53,17 @@ private static bool TryExpand16BitPng(byte[] compressedData, int width, int heig transparentBlue = colorType == 2 ? ReadUInt16BigEndian(transparency, 4) : -1; } - byte[] decoded = FlateDecoder.Decode(compressedData); + if (!TryDecodePngData(compressedData, out byte[] decoded, out unsupportedReason)) { + return false; + } + int sourceBytesPerPixel = sourceChannels * 2; - int expectedRowLength = 1 + width * sourceBytesPerPixel; - if (decoded.Length < expectedRowLength * height) { + if (!TryGetPngCheckedLength(width, height, sourceBytesPerPixel, includeFilterByte: true, out int expectedLength)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } + + if (decoded.Length < expectedLength) { unsupportedReason = "PNG image data ended before all 16-bit scanlines were decoded."; return false; } @@ -63,8 +72,14 @@ private static bool TryExpand16BitPng(byte[] compressedData, int width, int heig return false; } - byte[] baseRows = new byte[(1 + width * baseChannels) * height]; - byte[]? alphaRows = hasIntrinsicAlpha || transparency != null ? new byte[(1 + width) * height] : null; + if (!TryGetPngCheckedLength(width, height, baseChannels, includeFilterByte: true, out int baseRowsLength) || + !TryGetPngCheckedLength(width, height, 1, includeFilterByte: true, out int alphaRowsLength)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } + + byte[] baseRows = new byte[baseRowsLength]; + byte[]? alphaRows = hasIntrinsicAlpha || transparency != null ? new byte[alphaRowsLength] : null; for (int row = 0; row < height; row++) { int baseRowStart = row * (1 + width * baseChannels); diff --git a/OfficeIMO.Pdf/Rendering/Writer/PdfWriter.Images.cs b/OfficeIMO.Pdf/Rendering/Writer/PdfWriter.Images.cs index 0ab153448..46b941baf 100644 --- a/OfficeIMO.Pdf/Rendering/Writer/PdfWriter.Images.cs +++ b/OfficeIMO.Pdf/Rendering/Writer/PdfWriter.Images.cs @@ -1,3 +1,7 @@ +using System; +using System.IO; +using System.Text; +using System.Linq; using System.Globalization; using OfficeIMO.Drawing; using OfficeIMO.Pdf.Filters; @@ -5,6 +9,10 @@ namespace OfficeIMO.Pdf; internal static partial class PdfWriter { + private const long MaxPngPixelCount = 100_000_000L; + private const long MaxPngExpandedBytes = 256L * 1024L * 1024L; + private const int MaxPngDecodedBytes = 256 * 1024 * 1024; + internal sealed class PdfImageStream { public byte[] Data { get; set; } = Array.Empty(); public string DictionarySuffix { get; set; } = string.Empty; @@ -43,6 +51,11 @@ internal static bool TryGetPngImageData(byte[] data, out PdfImageStream image, o string type = Encoding.ASCII.GetString(data, offset + 4, 4); int chunkData = offset + 8; + if (!IsPngChunkCrcValid(data, offset, length)) { + unsupportedReason = "PNG chunk CRC is invalid."; + return false; + } + if (type == "IHDR") { if (length < 13) { unsupportedReason = "PNG IHDR chunk is invalid."; @@ -116,6 +129,10 @@ internal static bool TryGetPngImageData(byte[] data, out PdfImageStream image, o return false; } + if (!TryValidatePngResourceLimits(width, height, bitDepth, colorType, out unsupportedReason)) { + return false; + } + byte[] streamData = idat.ToArray(); if (interlace == 1 && !TryNormalizeAdam7PngData(streamData, width, height, bitDepth, colorType, out streamData, out unsupportedReason)) { @@ -198,8 +215,16 @@ private static bool TrySplitPngTransparency(byte[] compressedData, int width, in return false; } - byte[] decoded = FlateDecoder.Decode(compressedData); - if (decoded.Length < (1 + width * sourceChannels) * height) { + if (!TryDecodePngData(compressedData, out byte[] decoded, out unsupportedReason)) { + return false; + } + + if (!TryGetPngCheckedLength(width, height, sourceChannels, includeFilterByte: true, out int expectedLength)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } + + if (decoded.Length < expectedLength) { unsupportedReason = "PNG image data ended before all transparency scanlines were decoded."; return false; } @@ -208,8 +233,14 @@ private static bool TrySplitPngTransparency(byte[] compressedData, int width, in return false; } - byte[] baseRows = new byte[(1 + width * sourceChannels) * height]; - byte[] alphaRows = new byte[(1 + width) * height]; + if (!TryGetPngCheckedLength(width, height, sourceChannels, includeFilterByte: true, out int baseRowsLength) || + !TryGetPngCheckedLength(width, height, 1, includeFilterByte: true, out int alphaRowsLength)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } + + byte[] baseRows = new byte[baseRowsLength]; + byte[] alphaRows = new byte[alphaRowsLength]; int transparentGray = ReadUInt16BigEndian(transparency, 0); int transparentRed = colorType == 2 ? ReadUInt16BigEndian(transparency, 0) : -1; int transparentGreen = colorType == 2 ? ReadUInt16BigEndian(transparency, 2) : -1; @@ -271,9 +302,17 @@ private static bool TryExpandPackedGrayscalePng(byte[] compressedData, int width } } - byte[] decoded = FlateDecoder.Decode(compressedData); - int packedRowBytes = ((width * bitDepth) + 7) / 8; - if (decoded.Length < (packedRowBytes + 1) * height) { + if (!TryDecodePngData(compressedData, out byte[] decoded, out unsupportedReason)) { + return false; + } + + if (!TryGetPngRowByteCount(width, bitDepth, out int packedRowBytes) || + !TryGetPngScanlineLength(packedRowBytes, height, out int expectedLength)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } + + if (decoded.Length < expectedLength) { unsupportedReason = "PNG image data ended before all grayscale scanlines were decoded."; return false; } @@ -282,8 +321,13 @@ private static bool TryExpandPackedGrayscalePng(byte[] compressedData, int width return false; } - byte[] baseRows = new byte[(1 + width) * height]; - byte[]? alphaRows = transparency != null ? new byte[(1 + width) * height] : null; + if (!TryGetPngCheckedLength(width, height, 1, includeFilterByte: true, out int grayscaleRowsLength)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } + + byte[] baseRows = new byte[grayscaleRowsLength]; + byte[]? alphaRows = transparency != null ? new byte[grayscaleRowsLength] : null; for (int row = 0; row < height; row++) { int baseRowStart = row * (1 + width); int alphaRowStart = row * (1 + width); @@ -336,9 +380,17 @@ private static bool TryExpandIndexedPng(byte[] compressedData, int width, int he return false; } - byte[] decoded = FlateDecoder.Decode(compressedData); - int packedRowBytes = ((width * bitDepth) + 7) / 8; - if (decoded.Length < (packedRowBytes + 1) * height) { + if (!TryDecodePngData(compressedData, out byte[] decoded, out unsupportedReason)) { + return false; + } + + if (!TryGetPngRowByteCount(width, bitDepth, out int packedRowBytes) || + !TryGetPngScanlineLength(packedRowBytes, height, out int expectedLength)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } + + if (decoded.Length < expectedLength) { unsupportedReason = "PNG image data ended before all indexed-color scanlines were decoded."; return false; } @@ -347,8 +399,14 @@ private static bool TryExpandIndexedPng(byte[] compressedData, int width, int he return false; } - byte[] baseRows = new byte[(1 + width * 3) * height]; - byte[]? alphaRows = HasPaletteTransparency(paletteAlpha) ? new byte[(1 + width) * height] : null; + if (!TryGetPngCheckedLength(width, height, 3, includeFilterByte: true, out int rgbRowsLength) || + !TryGetPngCheckedLength(width, height, 1, includeFilterByte: true, out int indexedAlphaRowsLength)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } + + byte[] baseRows = new byte[rgbRowsLength]; + byte[]? alphaRows = HasPaletteTransparency(paletteAlpha) ? new byte[indexedAlphaRowsLength] : null; for (int row = 0; row < height; row++) { int baseRowStart = row * (1 + width * 3); int alphaRowStart = row * (1 + width); @@ -425,9 +483,15 @@ private static bool TrySplitPngAlpha(byte[] compressedData, int width, int heigh int sourceChannels = colorType == 4 ? 2 : 4; int baseChannels = colorType == 4 ? 1 : 3; - byte[] decoded = FlateDecoder.Decode(compressedData); - int expectedRowLength = 1 + width * sourceChannels; - int expectedLength = expectedRowLength * height; + if (!TryDecodePngData(compressedData, out byte[] decoded, out unsupportedReason)) { + return false; + } + + if (!TryGetPngCheckedLength(width, height, sourceChannels, includeFilterByte: true, out int expectedLength)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } + if (decoded.Length < expectedLength) { unsupportedReason = "PNG image data ended before all alpha scanlines were decoded."; return false; @@ -437,8 +501,14 @@ private static bool TrySplitPngAlpha(byte[] compressedData, int width, int heigh return false; } - byte[] baseRows = new byte[(1 + width * baseChannels) * height]; - byte[] alphaRows = new byte[(1 + width) * height]; + if (!TryGetPngCheckedLength(width, height, baseChannels, includeFilterByte: true, out int alphaBaseRowsLength) || + !TryGetPngCheckedLength(width, height, 1, includeFilterByte: true, out int splitAlphaRowsLength)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } + + byte[] baseRows = new byte[alphaBaseRowsLength]; + byte[] alphaRows = new byte[splitAlphaRowsLength]; for (int row = 0; row < height; row++) { int baseRowStart = row * (1 + width * baseChannels); int alphaRowStart = row * (1 + width); @@ -487,14 +557,20 @@ private static bool TryUnfilterPngRows(byte[] decoded, int width, int height, in rawPixels = Array.Empty(); unsupportedReason = null; - int stride = width * bytesPerPixel; - int sourceRowLength = stride + 1; - if (decoded.Length < sourceRowLength * height) { + if (!TryGetPngCheckedLength(width, height, bytesPerPixel, includeFilterByte: false, out int rawLength) || + !TryGetPngCheckedLength(width, height, bytesPerPixel, includeFilterByte: true, out int expectedLength)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } + + int stride = rawLength / height; + int sourceRowLength = expectedLength / height; + if (decoded.Length < expectedLength) { unsupportedReason = "PNG scanline data is incomplete."; return false; } - rawPixels = new byte[stride * height]; + rawPixels = new byte[rawLength]; byte[] previous = new byte[stride]; byte[] current = new byte[stride]; @@ -536,6 +612,67 @@ private static bool TryUnfilterPngRows(byte[] decoded, int width, int height, in return true; } + private static bool TryDecodePngData(byte[] compressedData, out byte[] decoded, out string? unsupportedReason) { + if (!FlateDecoder.TryDecode(compressedData, MaxPngDecodedBytes, out decoded)) { + unsupportedReason = "PNG image data exceeds the supported decompressed size limit."; + return false; + } + + unsupportedReason = null; + return true; + } + + private static bool TryValidatePngResourceLimits(int width, int height, int bitDepth, int colorType, out string? unsupportedReason) { + unsupportedReason = null; + long pixels = (long)width * height; + if (pixels > MaxPngPixelCount) { + unsupportedReason = "PNG dimensions exceed the supported pixel count limit."; + return false; + } + + if (!TryGetPngChannelCount(colorType, out int channels) || + !TryGetPngRowByteCount(width, channels * bitDepth, out int rowBytes) || + !TryGetPngScanlineLength(rowBytes, height, out int _)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } + + int expandedChannels = colorType == 3 || colorType == 6 ? 4 : Math.Max(channels, 1); + if (colorType == 4) { + expandedChannels = 2; + } + + if (!TryGetPngCheckedLength(width, height, expandedChannels, includeFilterByte: true, out int _)) { + unsupportedReason = "PNG dimensions exceed supported limits."; + return false; + } + + return true; + } + + private static bool TryGetPngCheckedLength(int width, int height, int channels, bool includeFilterByte, out int length) { + length = 0; + long rowLength = ((long)width * channels) + (includeFilterByte ? 1 : 0); + long totalLength = rowLength * height; + if (rowLength > int.MaxValue || totalLength > int.MaxValue || totalLength > MaxPngExpandedBytes) { + return false; + } + + length = (int)totalLength; + return true; + } + + private static bool TryGetPngScanlineLength(int rowBytes, int height, out int length) { + length = 0; + long totalLength = ((long)rowBytes + 1L) * height; + if (totalLength > int.MaxValue || totalLength > MaxPngExpandedBytes) { + return false; + } + + length = (int)totalLength; + return true; + } + private static int PaethPredictor(int left, int up, int upLeft) { int p = left + up - upLeft; int pa = Math.Abs(p - left); @@ -587,6 +724,34 @@ internal static PdfStream BuildImageXObject(PdfImageStream image, int? softMaskO return PdfImageXObjectDictionaryBuilder.BuildStreamObject(image, softMaskObjectNumber); } + private static bool IsPngChunkCrcValid(byte[] data, int chunkOffset, int chunkLength) { + int crcOffset = chunkOffset + 8 + chunkLength; + if (crcOffset + 4 > data.Length) { + return false; + } + + uint expectedCrc = ReadUInt32BigEndian(data, crcOffset); + uint actualCrc = Crc32(data, chunkOffset + 4, chunkLength + 4); + return expectedCrc == actualCrc; + } + + private static uint Crc32(byte[] data, int offset, int length) { + uint crc = 0xFFFFFFFF; + for (int i = 0; i < length; i++) { + crc ^= data[offset + i]; + for (int bit = 0; bit < 8; bit++) { + crc = (crc & 1) == 1 ? (crc >> 1) ^ 0xEDB88320U : crc >> 1; + } + } + + return ~crc; + } + + private static uint ReadUInt32BigEndian(byte[] data, int offset) => + offset + 4 <= data.Length + ? ((uint)data[offset] << 24) | ((uint)data[offset + 1] << 16) | ((uint)data[offset + 2] << 8) | data[offset + 3] + : 0; + private static bool IsPng(byte[] data) => data.Length >= 8 && data[0] == 137 && diff --git a/OfficeIMO.Tests/Pdf/PdfDocumentImageValidationTests.cs b/OfficeIMO.Tests/Pdf/PdfDocumentImageValidationTests.cs index 505755d91..350da20ba 100644 --- a/OfficeIMO.Tests/Pdf/PdfDocumentImageValidationTests.cs +++ b/OfficeIMO.Tests/Pdf/PdfDocumentImageValidationTests.cs @@ -1005,42 +1005,34 @@ private static double FindWordStartY(UglyToad.PdfPig.Content.Page page, string w } private static byte[] CreateMinimalRgbPng() { - return new byte[] { - 137, 80, 78, 71, 13, 10, 26, 10, - 0, 0, 0, 13, - 73, 72, 68, 82, + using var ms = new MemoryStream(); + byte[] signature = new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }; + ms.Write(signature, 0, signature.Length); + WritePngChunk(ms, "IHDR", new byte[] { 0, 0, 0, 1, 0, 0, 0, 1, - 8, 2, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 12, - 73, 68, 65, 84, - 0x78, 0x9C, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, - 0, 0, 0, 0, - 0, 0, 0, 0, - 73, 69, 78, 68, - 0, 0, 0, 0 - }; + 8, 2, 0, 0, 0 + }); + WritePngChunk(ms, "IDAT", new byte[] { 0x78, 0x9C, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00 }); + WritePngChunk(ms, "IEND", Array.Empty()); + return ms.ToArray(); } private static byte[] CreateMinimalRgbaPng() { - return new byte[] { - 137, 80, 78, 71, 13, 10, 26, 10, - 0, 0, 0, 13, - 73, 72, 68, 82, + using var ms = new MemoryStream(); + byte[] signature = new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }; + ms.Write(signature, 0, signature.Length); + WritePngChunk(ms, "IHDR", new byte[] { 0, 0, 0, 1, 0, 0, 0, 1, - 8, 6, 0, 0, 0, - 0, 0, 0, 0, - 0, 0, 0, 16, - 73, 68, 65, 84, + 8, 6, 0, 0, 0 + }); + WritePngChunk(ms, "IDAT", new byte[] { 0x78, 0x01, 0x01, 0x05, 0x00, 0xFA, 0xFF, 0x00, - 0xFF, 0x00, 0x00, 0x80, 0x04, 0x81, 0x01, 0x80, - 0, 0, 0, 0, - 0, 0, 0, 0, - 73, 69, 78, 68, - 0, 0, 0, 0 - }; + 0xFF, 0x00, 0x00, 0x80, 0x04, 0x81, 0x01, 0x80 + }); + WritePngChunk(ms, "IEND", Array.Empty()); + return ms.ToArray(); } private static byte[] CreateMinimalRgbTransparencyPng() { @@ -1182,7 +1174,11 @@ private static void WritePngChunk(Stream stream, string type, byte[] data) { byte[] typeBytes = System.Text.Encoding.ASCII.GetBytes(type); stream.Write(typeBytes, 0, typeBytes.Length); stream.Write(data, 0, data.Length); - stream.Write(new byte[] { 0, 0, 0, 0 }, 0, 4); + uint crc = ComputeCrc32(typeBytes, data); + stream.WriteByte((byte)((crc >> 24) & 0xFF)); + stream.WriteByte((byte)((crc >> 16) & 0xFF)); + stream.WriteByte((byte)((crc >> 8) & 0xFF)); + stream.WriteByte((byte)(crc & 0xFF)); } private static byte[] CreateMinimalGif() { @@ -1208,4 +1204,27 @@ private static byte[] CreateMinimalJpeg(int width, int height) { 0xFF, 0xD9 }; } + + private static uint ComputeCrc32(byte[] typeBytes, byte[] data) { + uint crc = 0xFFFFFFFF; + for (int i = 0; i < typeBytes.Length; i++) { + crc = UpdateCrc32(crc, typeBytes[i]); + } + + for (int i = 0; i < data.Length; i++) { + crc = UpdateCrc32(crc, data[i]); + } + + return crc ^ 0xFFFFFFFF; + } + + private static uint UpdateCrc32(uint crc, byte value) { + crc ^= value; + for (int bit = 0; bit < 8; bit++) { + crc = (crc & 1) != 0 ? (crc >> 1) ^ 0xEDB88320 : crc >> 1; + } + + return crc; + } + } diff --git a/OfficeIMO.Tests/Pdf/PdfDocumentPngImageTests.cs b/OfficeIMO.Tests/Pdf/PdfDocumentPngImageTests.cs index ff827ef95..9ea161735 100644 --- a/OfficeIMO.Tests/Pdf/PdfDocumentPngImageTests.cs +++ b/OfficeIMO.Tests/Pdf/PdfDocumentPngImageTests.cs @@ -167,6 +167,26 @@ public void Image_WithInterlacedIndexedPng_PreservesPaletteTransparencyPixels() Assert.Equal(PdfPngTestImages.CreateInterlacedIndexedRgbaExpectedScanlines(), PdfPngTestImages.DecodePngIdat(image.Bytes)); } + [Fact] + public void Image_WithOversizedInterlacedPng_RejectsImageBytesWithoutExpanding() { + Assert.False(PdfDocument.TryValidateImageBytes( + PdfPngTestImages.CreateOversizedInterlacedGrayscalePng(), + out OfficeImageInfo? imageInfo, + out string? unsupportedReason)); + Assert.Null(imageInfo); + Assert.Contains("PNG dimensions exceed", unsupportedReason, StringComparison.Ordinal); + } + + [Fact] + public void Image_WithInvalidPngCrc_RejectsImageBytes() { + Assert.False(PdfDocument.TryValidateImageBytes( + PdfPngTestImages.CreatePngWithInvalidCrc(), + out OfficeImageInfo? imageInfo, + out string? unsupportedReason)); + Assert.Null(imageInfo); + Assert.Contains("PNG chunk CRC is invalid.", unsupportedReason, StringComparison.Ordinal); + } + [Fact] public void Image_With16BitRgbTransparency_WritesSoftMaskImageObject() { byte[] bytes = PdfDocument.Create() diff --git a/OfficeIMO.Tests/Pdf/PdfPngTestImages.cs b/OfficeIMO.Tests/Pdf/PdfPngTestImages.cs index ca81ea58f..398e11228 100644 --- a/OfficeIMO.Tests/Pdf/PdfPngTestImages.cs +++ b/OfficeIMO.Tests/Pdf/PdfPngTestImages.cs @@ -128,6 +128,24 @@ internal static byte[] CreateInterlacedIndexedPng() { return ms.ToArray(); } + internal static byte[] CreateOversizedInterlacedGrayscalePng() { + using var ms = CreatePng(); + WritePngChunk(ms, "IHDR", new byte[] { + 0x00, 0x01, 0x86, 0xA0, + 0x00, 0x00, 0x75, 0x30, + 8, 0, 0, 0, 1 + }); + WritePngChunk(ms, "IDAT", BuildStoredZlib(Array.Empty())); + WritePngChunk(ms, "IEND", Array.Empty()); + return ms.ToArray(); + } + + internal static byte[] CreatePngWithInvalidCrc() { + byte[] png = Create16BitRgbPng(); + png[png.Length - 1] ^= 0xFF; + return png; + } + internal static byte[] CreateInterlacedRgbExpectedScanlines() { return CreateExpectedScanlines(3, WriteRgbPixel); } diff --git a/OfficeIMO.Tests/Pdf/PdfStamperSupport.cs b/OfficeIMO.Tests/Pdf/PdfStamperSupport.cs index fe2b47436..738997630 100644 --- a/OfficeIMO.Tests/Pdf/PdfStamperSupport.cs +++ b/OfficeIMO.Tests/Pdf/PdfStamperSupport.cs @@ -287,7 +287,33 @@ private static void WritePngChunk(Stream stream, string type, byte[] data) { byte[] typeBytes = Encoding.ASCII.GetBytes(type); stream.Write(typeBytes, 0, typeBytes.Length); stream.Write(data, 0, data.Length); - byte[] crc = new byte[] { 0, 0, 0, 0 }; - stream.Write(crc, 0, crc.Length); + uint crc = ComputeCrc32(typeBytes, data); + stream.WriteByte((byte)((crc >> 24) & 0xFF)); + stream.WriteByte((byte)((crc >> 16) & 0xFF)); + stream.WriteByte((byte)((crc >> 8) & 0xFF)); + stream.WriteByte((byte)(crc & 0xFF)); } + + private static uint ComputeCrc32(byte[] typeBytes, byte[] data) { + uint crc = 0xFFFFFFFF; + for (int i = 0; i < typeBytes.Length; i++) { + crc = UpdateCrc32(crc, typeBytes[i]); + } + + for (int i = 0; i < data.Length; i++) { + crc = UpdateCrc32(crc, data[i]); + } + + return crc ^ 0xFFFFFFFF; + } + + private static uint UpdateCrc32(uint crc, byte value) { + crc ^= value; + for (int bit = 0; bit < 8; bit++) { + crc = (crc & 1) != 0 ? (crc >> 1) ^ 0xEDB88320 : crc >> 1; + } + + return crc; + } + }