Skip to content

Commit

Permalink
Merge pull request #74 from neilharvey/bug-fix/malicious-file-formats
Browse files Browse the repository at this point in the history
Fix PDF header vulnerability
  • Loading branch information
neilharvey authored Nov 22, 2024
2 parents 56602a0 + 0812da6 commit ee8ef24
Show file tree
Hide file tree
Showing 13 changed files with 112 additions and 174 deletions.
4 changes: 2 additions & 2 deletions src/FileSignatures/FileFormatInspector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ public FileFormatInspector(IEnumerable<FileFormat> formats)
RemoveBaseFormats(matches);
}

if (matches.Count > 0)
if (matches.Count > 1)
{
return matches.OrderByDescending(m => m.HeaderLength).First();
}

return null;
return matches.Count == 1 ? matches[0] : null;
}

private List<FileFormat> FindMatchingFormats(Stream stream)
Expand Down
49 changes: 49 additions & 0 deletions src/FileSignatures/Formats/AdobePdf.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.IO;

namespace FileSignatures.Formats;

public class AdobePdf : Pdf
{
private const byte VersionNumberPlaceholder = 0x00;

public AdobePdf() : base([
0x25, 0x21, 0x50, 0x53, 0x2D, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x2D, VersionNumberPlaceholder, 0x2E,
VersionNumberPlaceholder, 0x20, 0x50, 0x44,
0x46, 0x2D, VersionNumberPlaceholder, 0x2E, VersionNumberPlaceholder
])
{
}

public override bool IsMatch(Stream stream)
{
if (stream == null || (stream.Length < HeaderLength && HeaderLength < int.MaxValue) || Offset > stream.Length)
{
return false;
}

stream.Position = Offset;

for (var i = 0; i < Signature.Count; i++)
{
var b = stream.ReadByte();
if (!IsSignatureByte(b, i))
{
return false;
}
}

return true;
}

protected bool IsSignatureByte(int value, int signatureIndex)
{
return IsVersionNumber(value, Signature[signatureIndex])
|| value == Signature[signatureIndex];
}

private static bool IsVersionNumber(int value, byte signatureByte)
{
var isNumber = value is >= 0x30 and <= 0x39;
return signatureByte == VersionNumberPlaceholder && isNumber;
}
}
45 changes: 45 additions & 0 deletions src/FileSignatures/Formats/OpenDocument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Text;

namespace FileSignatures.Formats;

/// <summary>
/// Specifies the format of an OpenDocument file.
/// </summary>
public abstract class OpenDocument : Zip
{
private const int SubsignatureOffset = 30;

protected OpenDocument(byte[] subsignature, string mediaType, string extension) : base(SubsignatureOffset + subsignature.Length, mediaType, extension)
{
Subsignature = new ReadOnlyCollection<byte>(subsignature); ;
}

/// <summary>
/// Gets the subsignature of the OpenDocument file.
/// This will be the media type in byte format e.g. 'mimetypeapplication/vnd.oasis.opendocument.presentation'
/// </summary>
public ReadOnlyCollection<byte> Subsignature { get; }

public override bool IsMatch(Stream stream)
{
if (!base.IsMatch(stream))
return false;

stream.Position = SubsignatureOffset;

for (int i = 0; i < Subsignature.Count; i++)
{
var b = stream.ReadByte();
if (b != Subsignature[i])
{
return false;
}
}

return true;
}
}
8 changes: 4 additions & 4 deletions src/FileSignatures/Formats/OpenDocumentPresentation.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
namespace FileSignatures.Formats
{
/// <summary>
/// Specifies the format of a OffsetFileFormat file.
/// Specifies the format of a OpenDocument Presentation file.
/// </summary>
public class OpenDocumentPresentation : FileFormat
public class OpenDocumentPresentation : OpenDocument
{
public OpenDocumentPresentation() : base(new byte[] {
public OpenDocumentPresentation() : base([
0x6D, 0x69, 0x6D, 0x65, 0x74, 0x79, 0x70, 0x65, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E,
0x2F, 0x76, 0x6E, 0x64, 0x2E, 0x6F, 0x61, 0x73, 0x69, 0x73, 0x2E, 0x6F, 0x70, 0x65, 0x6E, 0x64, 0x6F, 0x63, 0x75,
0x6D, 0x65, 0x6E, 0x74, 0x2E, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6E, 0x74, 0x61, 0x74, 0x69, 0x6F, 0x6E
}, "application/vnd.oasis.opendocument.presentation", "odp", 30)
], "application/vnd.oasis.opendocument.presentation", "odp")
{
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/FileSignatures/Formats/OpenDocumentSpreadsheet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
/// <summary>
/// Specifies the format of a OffsetFileFormat file.
/// </summary>
public class OpenDocumentSpreadsheet : FileFormat
public class OpenDocumentSpreadsheet : OpenDocument
{
public OpenDocumentSpreadsheet() : base(new byte[] {
public OpenDocumentSpreadsheet() : base([
0x6D, 0x69, 0x6D, 0x65, 0x74, 0x79, 0x70, 0x65, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F,
0x6E, 0x2F, 0x76, 0x6E, 0x64, 0x2E, 0x6F, 0x61, 0x73, 0x69, 0x73, 0x2E, 0x6F, 0x70, 0x65, 0x6E, 0x64, 0x6F,
0x63, 0x75, 0x6D, 0x65, 0x6E, 0x74, 0x2E, 0x73, 0x70, 0x72, 0x65, 0x61, 0x64, 0x73, 0x68, 0x65, 0x65, 0x74
}, "application/vnd.oasis.opendocument.spreadsheet", "ods", 30)
], "application/vnd.oasis.opendocument.spreadsheet", "ods")
{
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/FileSignatures/Formats/OpenDocumentText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
/// <summary>
/// Specifies the format of a OpenDocumentText file.
/// </summary>
public class OpenDocumentText : FileFormat
public class OpenDocumentText : OpenDocument
{
public OpenDocumentText() : base(new byte[] {
public OpenDocumentText() : base([
0x6D, 0x69, 0x6D, 0x65, 0x74, 0x79, 0x70, 0x65, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74,
0x69, 0x6F, 0x6E, 0x2F, 0x76, 0x6E, 0x64, 0x2E, 0x6F, 0x61, 0x73, 0x69, 0x73, 0x2E, 0x6F, 0x70,
0x65, 0x6E, 0x64, 0x6F, 0x63, 0x75, 0x6D, 0x65, 0x6E, 0x74, 0x2E, 0x74, 0x65, 0x78, 0x74
}, "application/vnd.oasis.opendocument.text", "odt", 30)
], "application/vnd.oasis.opendocument.text", "odt")
{
}
}
Expand Down
65 changes: 1 addition & 64 deletions src/FileSignatures/Formats/Pdf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,76 +7,13 @@ namespace FileSignatures.Formats
/// </summary>
public class Pdf : FileFormat
{
private const uint MaxFileHeaderSize = 1024;

public Pdf() : this([0x25, 0x50, 0x44, 0x46])
{
}

protected Pdf(byte[] signature) : base(signature, "application/pdf", "pdf", 0)
{
}

public override bool IsMatch(Stream stream)
{
if (stream == null || stream.Length < HeaderLength)
{
return false;
}

stream.Position = 0;
var signatureValidationIndex = 0;
int fileByte;

while (stream.Position < MaxFileHeaderSize && (fileByte = stream.ReadByte()) != -1)
{
if (CompareFileByteToSignatureAt((byte)fileByte, signatureValidationIndex))
{
signatureValidationIndex++;
}
else
{
signatureValidationIndex = 0;
}

if (signatureValidationIndex == Signature.Count)
return true;
}

return false;
}

protected virtual bool CompareFileByteToSignatureAt(byte fileByte, int signatureIndex)
{
return fileByte == Signature[signatureIndex];
}
}

public class AdobePdf : Pdf
{
private const byte VersionNumberPlaceholder = 0x00;

public AdobePdf() : base([
0x25, 0x21, 0x50, 0x53, 0x2D, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x2D, VersionNumberPlaceholder, 0x2E,
VersionNumberPlaceholder, 0x20, 0x50, 0x44,
0x46, 0x2D, VersionNumberPlaceholder, 0x2E, VersionNumberPlaceholder
])
{
}

protected override bool CompareFileByteToSignatureAt(byte fileByte, int signatureIndex)
{
return base.CompareFileByteToSignatureAt(fileByte, signatureIndex) || IsVersionNumber(fileByte, Signature[signatureIndex]);
}

private static bool IsVersionNumber(byte fileByte, byte signatureByte)
{
return signatureByte == VersionNumberPlaceholder && IsNumber(fileByte);
}

private static bool IsNumber(byte @byte)
{
return @byte is >= 0x30 and <= 0x39;
}

}
}
90 changes: 0 additions & 90 deletions test/FileSignatures.Tests/Formats/PdfTests.cs

This file was deleted.

13 changes: 5 additions & 8 deletions test/FileSignatures.Tests/FunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ public class FunctionalTests
[InlineData("dragndrop.msg", "application/vnd.ms-outlook")]
[InlineData("nonstandard.docx","application/vnd.openxmlformats-officedocument.wordprocessingml.document")]
[InlineData("test.pdf", "application/pdf")]
[InlineData("test_header_somewhere_in_1024_first_bytes.pdf", "application/pdf")]
[InlineData("test_header_adobe.pdf", "application/pdf")]
[InlineData("adobe.pdf", "application/pdf")]
[InlineData("test.rtf", "application/rtf")]
[InlineData("test.png", "image/png")]
[InlineData("test.ppt", "application/vnd.ms-powerpoint")]
Expand Down Expand Up @@ -52,6 +51,7 @@ public class FunctionalTests
[InlineData("test.ogg", "audio/ogg")]
[InlineData("test.amr", "audio/amr")]
[InlineData("test.ico", "image/vnd.microsoft.icon")]
[InlineData("malicious.pdf", "application/vnd.microsoft.portable-executable")]
public void SamplesAreRecognised(string sample, string expected)
{
var result = InspectSample(sample);
Expand All @@ -63,14 +63,11 @@ public void SamplesAreRecognised(string sample, string expected)
private static FileFormat InspectSample(string fileName)
{
var inspector = new FileFormatInspector();
var buildDirectoryPath = Path.GetDirectoryName(typeof(FunctionalTests).GetTypeInfo().Assembly.Location);
var buildDirectoryPath = Path.GetDirectoryName(typeof(FunctionalTests).GetTypeInfo().Assembly.Location)!;
var sample = new FileInfo(Path.Combine(buildDirectoryPath, "Samples", fileName));
FileFormat result;

using (var stream = sample.OpenRead())
{
result = inspector.DetermineFileFormat(stream);
}
using var stream = sample.OpenRead();
var result = inspector.DetermineFileFormat(stream);

return result;
}
Expand Down
Binary file added test/FileSignatures.Tests/Samples/adobe.pdf
Binary file not shown.
Binary file added test/FileSignatures.Tests/Samples/malicious.pdf
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 comments on commit ee8ef24

Please sign in to comment.