From 5fcf89dff16619bc7ab4652ee644f0777ff32295 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 29 Aug 2017 16:16:04 -0400 Subject: [PATCH] Ignore "GIF of death." // FREEBIE --- Signal.xcodeproj/project.pbxproj | 14 +- .../TSMessageAdapaters/TSAnimatedAdapter.m | 14 ++ Signal/src/util/NSData+Image.h | 12 ++ Signal/src/util/NSData+Image.m | 139 ++++++++++++++++++ .../OWSOutgoingMessageCollectionViewCell.m | 1 - 5 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 Signal/src/util/NSData+Image.h create mode 100644 Signal/src/util/NSData+Image.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index b4b3acde8..ee6b4a9d6 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ 345671011E89A5F1006EE662 /* ThreadUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 345671001E89A5F1006EE662 /* ThreadUtil.m */; }; 3456710A1E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = 345671091E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m */; }; 346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */; }; + 346B66381F55E24900E5122F /* NSData+Image.m in Sources */ = {isa = PBXBuildFile; fileRef = 346B66371F55E24900E5122F /* NSData+Image.m */; }; 3471B1DA1EB7C63600F6AEC8 /* NewNonContactConversationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3471B1D91EB7C63600F6AEC8 /* NewNonContactConversationViewController.m */; }; 3472229F1EB22FFE00E53955 /* AddToGroupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3472229E1EB22FFE00E53955 /* AddToGroupViewController.m */; }; 348F2EAE1F0D21BC00D4ECE0 /* DeviceSleepManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 348F2EAD1F0D21BC00D4ECE0 /* DeviceSleepManager.swift */; }; @@ -445,6 +446,8 @@ 345671081E8A9F5D006EE662 /* TSGenericAttachmentAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSGenericAttachmentAdapter.h; sourceTree = ""; }; 345671091E8A9F5D006EE662 /* TSGenericAttachmentAdapter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSGenericAttachmentAdapter.m; sourceTree = ""; }; 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = ""; }; + 346B66361F55E24900E5122F /* NSData+Image.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+Image.h"; sourceTree = ""; }; + 346B66371F55E24900E5122F /* NSData+Image.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Image.m"; sourceTree = ""; }; 3471B1D81EB7C63600F6AEC8 /* NewNonContactConversationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NewNonContactConversationViewController.h; sourceTree = ""; }; 3471B1D91EB7C63600F6AEC8 /* NewNonContactConversationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NewNonContactConversationViewController.m; sourceTree = ""; }; 3472229D1EB22FFE00E53955 /* AddToGroupViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AddToGroupViewController.h; sourceTree = ""; }; @@ -1422,6 +1425,9 @@ 45E615151E8C590B0018AD52 /* DisplayableTextFilter.swift */, 76EB04EA18170B33006006FC /* FunctionalUtil.h */, 76EB04EB18170B33006006FC /* FunctionalUtil.m */, + 455AC69A1F4F79E500134004 /* ImageCache.swift */, + 346B66361F55E24900E5122F /* NSData+Image.h */, + 346B66371F55E24900E5122F /* NSData+Image.m */, B62F5E0E1C2980B4000D370C /* NSData+ows_StripToken.h */, B62F5E0F1C2980B4000D370C /* NSData+ows_StripToken.m */, 76EB04EC18170B33006006FC /* NumberUtil.h */, @@ -1455,7 +1461,6 @@ 76EB04FB18170B33006006FC /* Util.h */, 45F170D51E315310003FC1F2 /* Weak.swift */, 45F170CB1E310E22003FC1F2 /* WeakTimer.swift */, - 455AC69A1F4F79E500134004 /* ImageCache.swift */, ); path = util; sourceTree = ""; @@ -2236,6 +2241,7 @@ 45794E861E00620000066731 /* CallUIAdapter.swift in Sources */, 4585C4681ED8F8D200896AEA /* SafetyNumberConfirmationAlert.swift in Sources */, FCFA64B71A24F6730007FB87 /* UIFont+OWS.m in Sources */, + 346B66381F55E24900E5122F /* NSData+Image.m in Sources */, B6B9ECFC198B31BA00C620D3 /* PushManager.m in Sources */, 45DF5DF21DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift in Sources */, 458DE9D91DEE7B360071BB03 /* OWSWebRTCDataProtos.pb.m in Sources */, @@ -2580,7 +2586,11 @@ "DEBUG=1", "$(inherited)", ); - "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = "DEBUG=1 $(inherited) SSK_BUILDING_FOR_TESTS=1"; + "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = ( + "DEBUG=1", + "$(inherited)", + "SSK_BUILDING_FOR_TESTS=1", + ); GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.m b/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.m index 9d2a73787..92b2f3fc9 100644 --- a/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.m +++ b/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.m @@ -6,6 +6,7 @@ #import "AttachmentUploadView.h" #import "FLAnimatedImage.h" #import "JSQMediaItem+OWS.h" +#import "NSData+Image.h" #import "TSAttachmentStream.h" #import "UIColor+OWS.h" #import @@ -27,6 +28,8 @@ NS_ASSUME_NONNULL_BEGIN // See comments on OWSMessageMediaAdapter. @property (nonatomic, nullable, weak) id lastPresentingCell; +@property (nonatomic) NSNumber *isImageValid; + @end #pragma mark - @@ -96,10 +99,21 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - JSQMessageMediaData protocol +- (NSNumber *)isImageValid +{ + if (!_isImageValid) { + _isImageValid = @([NSData isValidImageAtPath:[self.attachment mediaURL].path]); + } + return _isImageValid; +} + - (UIView *)mediaView { OWSAssert([NSThread isMainThread]); if (self.cachedImageView == nil) { + if (![self isImageValid]) { + return nil; + } // Use Flipboard FLAnimatedImage library to display gifs NSData *fileData = [NSData dataWithContentsOfURL:[self.attachment mediaURL]]; if (!fileData) { diff --git a/Signal/src/util/NSData+Image.h b/Signal/src/util/NSData+Image.h new file mode 100644 index 000000000..aaf545ac5 --- /dev/null +++ b/Signal/src/util/NSData+Image.h @@ -0,0 +1,12 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +@interface NSData (Image) + ++ (BOOL)isValidImageAtPath:(NSString *)filePath; +- (BOOL)isValidImage; + +@end diff --git a/Signal/src/util/NSData+Image.m b/Signal/src/util/NSData+Image.m new file mode 100644 index 000000000..bda89a542 --- /dev/null +++ b/Signal/src/util/NSData+Image.m @@ -0,0 +1,139 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "NSData+Image.h" + +typedef NS_ENUM(NSInteger, ImageFormat) { + ImageFormat_Unknown, + ImageFormat_Png, + ImageFormat_Gif, + ImageFormat_Tiff, + ImageFormat_Jpeg, + ImageFormat_Bmp, +}; + +@implementation NSData (Image) + ++ (BOOL)isValidImageAtPath:(NSString *)filePath +{ + NSError *error = nil; + NSData *data = [NSData dataWithContentsOfFile:filePath options:NSMappedRead error:&error]; + if (error) { + DDLogError(@"%@ could not read image data: %@", self.tag, error); + } + + return [data isValidImage]; +} + +- (BOOL)isValidImage +{ + // Don't trust the file extension; iOS (e.g. UIKit, Core Graphics) will happily + // load a .gif with a .png file extension. + // + // Instead, use the "magic numbers" in the file data to determine the image format. + ImageFormat imageFormat = [self guessImageFormat]; + if (imageFormat == ImageFormat_Gif) { + return [self hasValidGifSize]; + } else if (imageFormat == ImageFormat_Unknown) { + return NO; + } else { + return YES; + } +} + +- (ImageFormat)guessImageFormat +{ + const NSUInteger kTwoBytesLength = 2; + if (self.length < kTwoBytesLength) { + return ImageFormat_Unknown; + } + + unsigned char bytes[kTwoBytesLength]; + [self getBytes:&bytes range:NSMakeRange(0, kTwoBytesLength)]; + + unsigned char byte0 = bytes[0]; + unsigned char byte1 = bytes[1]; + + if (byte0 == 0x47 && byte1 == 0x49) { + return ImageFormat_Gif; + } else if (byte0 == 0x89 && byte1 == 0x50) { + return ImageFormat_Png; + } else if (byte0 == 0xff && byte1 == 0xd8) { + return ImageFormat_Jpeg; + } else if (byte0 == 0x42 && byte1 == 0x4d) { + return ImageFormat_Bmp; + } else if (byte0 == 0x4D && byte1 == 0x4D) { + // Motorola byte order TIFF + return ImageFormat_Tiff; + } else if (byte0 == 0x49 && byte1 == 0x49) { + // Intel byte order TIFF + return ImageFormat_Tiff; + } + + return ImageFormat_Unknown; +} + ++ (BOOL)areByteArraysEqual:(NSUInteger)length left:(unsigned char *)left right:(unsigned char *)right +{ + for (NSUInteger i = 0; i < length; i++) { + if (left[i] != right[i]) { + return NO; + } + } + return YES; +} + +// Parse the GIF header to prevent the "GIF of death" issue. +// +// See: https://blog.flanker017.me/cve-2017-2416-gif-remote-exec/ +- (BOOL)hasValidGifSize +{ + const NSUInteger kSignatureLength = 3; + const NSUInteger kVersionLength = 3; + const NSUInteger kWidthLength = 2; + const NSUInteger kHeightLength = 2; + const NSUInteger kPrefixLength = kSignatureLength + kVersionLength; + const NSUInteger kBufferLength = kSignatureLength + kVersionLength + kWidthLength + kHeightLength; + + if (self.length < kBufferLength) { + return NO; + } + + unsigned char bytes[kBufferLength]; + [self getBytes:&bytes range:NSMakeRange(0, kBufferLength)]; + + unsigned char kGif87APrefix[kPrefixLength] = { + 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, + }; + unsigned char kGif89APrefix[kPrefixLength] = { + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, + }; + if (![NSData areByteArraysEqual:kPrefixLength left:bytes right:kGif87APrefix] + && ![NSData areByteArraysEqual:kPrefixLength left:bytes right:kGif89APrefix]) { + return NO; + } + NSUInteger width = ((NSUInteger)bytes[kPrefixLength + 0]) | (((NSUInteger)bytes[kPrefixLength + 1] << 8)); + NSUInteger height = ((NSUInteger)bytes[kPrefixLength + 2]) | (((NSUInteger)bytes[kPrefixLength + 3] << 8)); + + // We need to ensure that the image size is "reasonable". + // We impose an arbitrary "very large" limit on image size + // to eliminate harmful values. + const NSUInteger kMaxValidSize = 1 << 18; + + return (width > 0 && width < kMaxValidSize && height > 0 && height < kMaxValidSize); +} + +#pragma mark - Logging + ++ (NSString *)tag +{ + return [NSString stringWithFormat:@"[%@]", self.class]; +} + +- (NSString *)tag +{ + return self.class.tag; +} + +@end diff --git a/Signal/src/views/OWSOutgoingMessageCollectionViewCell.m b/Signal/src/views/OWSOutgoingMessageCollectionViewCell.m index 81e3d1d09..d3655602e 100644 --- a/Signal/src/views/OWSOutgoingMessageCollectionViewCell.m +++ b/Signal/src/views/OWSOutgoingMessageCollectionViewCell.m @@ -3,7 +3,6 @@ // #import "OWSOutgoingMessageCollectionViewCell.h" -#import "FLAnimatedImageView.h" #import "OWSExpirationTimerView.h" #import "UIView+OWS.h" #import