Skip to content
Closed
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
76 changes: 63 additions & 13 deletions OfficeIMO.Pdf/Reading/Filters/FlateDecoder.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using System.IO;
using System.IO.Compression;

namespace OfficeIMO.Pdf.Filters;
Expand All @@ -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<byte>();
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;
Comment on lines +52 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop falling back after hitting the inflate limit

When a PNG IDAT/deflate stream expands beyond MaxPngDecodedBytes, TryCopyToByteArray returns false, but this fallback still returns the compressed input whenever the compressed size is under the limit. The PNG callers then treat those bytes as decoded scanlines, so a crafted oversized payload with a legal filter byte can bypass the new decompressed-size rejection instead of failing; track a limit-exceeded result separately and do not fall back in that case.

Useful? React with 👍 / 👎.

}

output = Array.Empty<byte>();
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;
Expand Down
46 changes: 36 additions & 10 deletions OfficeIMO.Pdf/Rendering/Writer/PdfWriter.Images.Interlace.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using System.IO;
using OfficeIMO.Pdf.Filters;

namespace OfficeIMO.Pdf;
Expand All @@ -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++) {
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
25 changes: 20 additions & 5 deletions OfficeIMO.Pdf/Rendering/Writer/PdfWriter.Images.Png16.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using System.IO;
using OfficeIMO.Pdf.Filters;

namespace OfficeIMO.Pdf;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand Down
Loading
Loading