diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index b4b3acde8..ac4c1d9a7 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -1422,6 +1422,7 @@ 45E615151E8C590B0018AD52 /* DisplayableTextFilter.swift */, 76EB04EA18170B33006006FC /* FunctionalUtil.h */, 76EB04EB18170B33006006FC /* FunctionalUtil.m */, + 455AC69A1F4F79E500134004 /* ImageCache.swift */, B62F5E0E1C2980B4000D370C /* NSData+ows_StripToken.h */, B62F5E0F1C2980B4000D370C /* NSData+ows_StripToken.m */, 76EB04EC18170B33006006FC /* NumberUtil.h */, @@ -1455,7 +1456,6 @@ 76EB04FB18170B33006006FC /* Util.h */, 45F170D51E315310003FC1F2 /* Weak.swift */, 45F170CB1E310E22003FC1F2 /* WeakTimer.swift */, - 455AC69A1F4F79E500134004 /* ImageCache.swift */, ); path = util; sourceTree = ""; @@ -2580,7 +2580,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/JSQMediaItem+OWS.m b/Signal/src/Models/TSMessageAdapaters/JSQMediaItem+OWS.m index da6ff7cfe..2f797ae1c 100644 --- a/Signal/src/Models/TSMessageAdapaters/JSQMediaItem+OWS.m +++ b/Signal/src/Models/TSMessageAdapaters/JSQMediaItem+OWS.m @@ -6,6 +6,7 @@ #import "NumberUtil.h" #import "UIDevice+TSHardwareVersion.h" #import +#import @implementation JSQMediaItem (OWS) @@ -42,6 +43,10 @@ { OWSAssert(imageURL); + if (![NSData ows_isValidImageAtPath:imageURL.path]) { + return CGSizeZero; + } + // With CGImageSource we avoid loading the whole image into memory. CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)imageURL, NULL); if (!source) { diff --git a/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.m b/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.m index 9d2a73787..35d2204fa 100644 --- a/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.m +++ b/Signal/src/Models/TSMessageAdapaters/TSAnimatedAdapter.m @@ -12,6 +12,7 @@ #import #import #import +#import NS_ASSUME_NONNULL_BEGIN @@ -108,6 +109,9 @@ NS_ASSUME_NONNULL_BEGIN view.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f]; return view; } + if (![fileData ows_isValidImage]) { + return nil; + } FLAnimatedImage *animatedGif = [FLAnimatedImage animatedImageWithGIFData:fileData]; FLAnimatedImageView *imageView = [[FLAnimatedImageView alloc] init]; imageView.animatedImage = animatedGif; diff --git a/Signal/src/Profiles/OWSProfileManager.m b/Signal/src/Profiles/OWSProfileManager.m index 605ca79b8..5ef2fac70 100644 --- a/Signal/src/Profiles/OWSProfileManager.m +++ b/Signal/src/Profiles/OWSProfileManager.m @@ -7,6 +7,7 @@ #import "NSString+OWS.h" #import "Signal-Swift.h" #import +#import #import #import #import @@ -1333,7 +1334,11 @@ const NSUInteger kOWSProfileManager_MaxAvatarDiameter = 640; { OWSAssert(filename.length > 0); - UIImage *_Nullable image = [UIImage imageWithData:[self loadProfileDataWithFilename:filename]]; + NSData *data = [self loadProfileDataWithFilename:filename]; + if (![data ows_isValidImage]) { + return nil; + } + UIImage *_Nullable image = [UIImage imageWithData:data]; return image; } diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 960c613b2..4eecc6337 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -48,6 +48,7 @@ #import #import #import +#import #import #import #import diff --git a/Signal/src/ViewControllers/AttachmentApprovalViewController.swift b/Signal/src/ViewControllers/AttachmentApprovalViewController.swift index 20d843daf..45441c887 100644 --- a/Signal/src/ViewControllers/AttachmentApprovalViewController.swift +++ b/Signal/src/ViewControllers/AttachmentApprovalViewController.swift @@ -171,8 +171,13 @@ class AttachmentApprovalViewController: OWSViewController, OWSAudioAttachmentPla } private func createAnimatedPreview(attachmentPreviewView: UIView) { + let data = attachment.data + guard (data as NSData).ows_isValidImage() else { + return + } + // Use Flipboard FLAnimatedImage library to display gifs - guard let animatedImage = FLAnimatedImage(gifData:attachment.data) else { + guard let animatedImage = FLAnimatedImage(gifData:data) else { createGenericPreview(attachmentPreviewView:attachmentPreviewView) return } diff --git a/Signal/src/ViewControllers/FullImageViewController.m b/Signal/src/ViewControllers/FullImageViewController.m index 8f87b34e7..cc83f00d2 100644 --- a/Signal/src/ViewControllers/FullImageViewController.m +++ b/Signal/src/ViewControllers/FullImageViewController.m @@ -2,15 +2,16 @@ // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // -#import "FLAnimatedImage.h" #import "FullImageViewController.h" -#import "UIUtil.h" -#import "UIView+OWS.h" -#import "TSPhotoAdapter.h" -#import "TSMessageAdapter.h" +#import "AttachmentSharing.h" +#import "FLAnimatedImage.h" #import "TSAnimatedAdapter.h" +#import "TSMessageAdapter.h" +#import "TSPhotoAdapter.h" #import "UIColor+OWS.h" -#import "AttachmentSharing.h" +#import "UIUtil.h" +#import "UIView+OWS.h" +#import NS_ASSUME_NONNULL_BEGIN @@ -168,14 +169,18 @@ NS_ASSUME_NONNULL_BEGIN - (void)initializeImageView { if (self.isAnimated) { - // Present the animated image using Flipboard/FLAnimatedImage - FLAnimatedImage *animatedGif = [FLAnimatedImage animatedImageWithGIFData:self.fileData]; - FLAnimatedImageView *imageView = [[FLAnimatedImageView alloc] init]; - imageView.animatedImage = animatedGif; - imageView.frame = self.originRect; - imageView.contentMode = UIViewContentModeScaleAspectFill; - imageView.clipsToBounds = YES; - self.imageView = imageView; + if ([self.fileData ows_isValidImage]) { + // Present the animated image using Flipboard/FLAnimatedImage + FLAnimatedImage *animatedGif = [FLAnimatedImage animatedImageWithGIFData:self.fileData]; + FLAnimatedImageView *imageView = [[FLAnimatedImageView alloc] init]; + imageView.animatedImage = animatedGif; + imageView.frame = self.originRect; + imageView.contentMode = UIViewContentModeScaleAspectFill; + imageView.clipsToBounds = YES; + self.imageView = imageView; + } else { + self.imageView = [[UIImageView alloc] initWithFrame:self.originRect]; + } } else { // Present the static image using standard UIImageView self.imageView = [[UIImageView alloc] initWithFrame:self.originRect]; diff --git a/Signal/src/util/UIUtil.h b/Signal/src/util/UIUtil.h index 7f001a8ae..d2aea1262 100644 --- a/Signal/src/util/UIUtil.h +++ b/Signal/src/util/UIUtil.h @@ -1,9 +1,12 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + #import #import "MIMETypeUtil.h" #import "UIColor+OWS.h" #import "UIFont+OWS.h" -#import "UIImage+contentTypes.h" #import "UIImage+normalizeImage.h" typedef void (^completionBlock)(void); 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 diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index 74bc26eda..730138a0f 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -4,6 +4,7 @@ #import "TSAttachmentStream.h" #import "MIMETypeUtil.h" +#import "NSData+Image.h" #import "TSAttachmentPointer.h" #import #import @@ -257,7 +258,11 @@ NS_ASSUME_NONNULL_BEGIN if (!mediaUrl) { return nil; } - return [UIImage imageWithData:[NSData dataWithContentsOfURL:mediaUrl]]; + NSData *data = [NSData dataWithContentsOfURL:mediaUrl]; + if (![data ows_isValidImage]) { + return nil; + } + return [UIImage imageWithData:data]; } else { return nil; } @@ -314,6 +319,9 @@ NS_ASSUME_NONNULL_BEGIN if (!mediaUrl) { return CGSizeZero; } + if (![NSData ows_isValidImageAtPath:mediaUrl.path]) { + return CGSizeZero; + } // With CGImageSource we avoid loading the whole image into memory. CGImageSourceRef source = CGImageSourceCreateWithURL((CFURLRef)mediaUrl, NULL); diff --git a/SignalServiceKit/src/Util/MIMETypeUtil.m b/SignalServiceKit/src/Util/MIMETypeUtil.m index e3f811dcd..4bcbe3ffc 100644 --- a/SignalServiceKit/src/Util/MIMETypeUtil.m +++ b/SignalServiceKit/src/Util/MIMETypeUtil.m @@ -4,7 +4,6 @@ #import "MIMETypeUtil.h" #if TARGET_OS_IPHONE -#import "UIImage+contentTypes.h" #import #else #import diff --git a/SignalServiceKit/src/Util/NSData+Image.h b/SignalServiceKit/src/Util/NSData+Image.h new file mode 100644 index 000000000..451f90465 --- /dev/null +++ b/SignalServiceKit/src/Util/NSData+Image.h @@ -0,0 +1,12 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +@interface NSData (Image) + ++ (BOOL)ows_isValidImageAtPath:(NSString *)filePath; +- (BOOL)ows_isValidImage; + +@end diff --git a/SignalServiceKit/src/Util/NSData+Image.m b/SignalServiceKit/src/Util/NSData+Image.m new file mode 100644 index 000000000..987f54ee1 --- /dev/null +++ b/SignalServiceKit/src/Util/NSData+Image.m @@ -0,0 +1,140 @@ +// +// 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)ows_isValidImageAtPath:(NSString *)filePath +{ + NSError *error = nil; + NSData *data = [NSData dataWithContentsOfFile:filePath options:NSDataReadingMappedIfSafe error:&error]; + if (error) { + DDLogError(@"%@ could not read image data: %@", self.tag, error); + } + + return [data ows_isValidImage]; +} + +- (BOOL)ows_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 ows_guessImageFormat]; + if (imageFormat == ImageFormat_Gif) { + return [self ows_hasValidGifSize]; + } else if (imageFormat == ImageFormat_Unknown) { + return NO; + } else { + return YES; + } +} + +- (ImageFormat)ows_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)ows_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/ +// See: https://www.w3.org/Graphics/GIF/spec-gif89a.txt +- (BOOL)ows_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 ows_areByteArraysEqual:kPrefixLength left:bytes right:kGif87APrefix] + && ![NSData ows_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/SignalServiceKit/src/Util/UIImage+contentTypes.h b/SignalServiceKit/src/Util/UIImage+contentTypes.h deleted file mode 100644 index 6c56cb4cf..000000000 --- a/SignalServiceKit/src/Util/UIImage+contentTypes.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// UIImage+contentTypes.h -// Signal -// -// Created by Frederic Jacobs on 21/12/14. -// Copyright (c) 2014 Open Whisper Systems. All rights reserved. -// - -#import - -@interface UIImage (contentTypes) - -- (NSString *)contentType; -- (BOOL)isSupportedImageType; - -@end diff --git a/SignalServiceKit/src/Util/UIImage+contentTypes.m b/SignalServiceKit/src/Util/UIImage+contentTypes.m deleted file mode 100644 index d52bb450a..000000000 --- a/SignalServiceKit/src/Util/UIImage+contentTypes.m +++ /dev/null @@ -1,34 +0,0 @@ -// Created by Frederic Jacobs on 21/12/14. -// Copyright (c) 2014 Open Whisper Systems. All rights reserved. - -#import "MIMETypeUtil.h" -#import "UIImage+contentTypes.h" - -@implementation UIImage (contentTypes) - -- (NSString *)contentType { - uint8_t c; - [UIImagePNGRepresentation(self) getBytes:&c length:1]; - - switch (c) { - case 0xFF: - return @"image/jpeg"; - case 0x89: - return OWSMimeTypeImagePng; - case 0x47: - return @"image/gif"; - case 0x49: - break; - case 0x42: - return @"image/bmp"; - case 0x4D: - return @"image/tiff"; - } - return nil; -} - -- (BOOL)isSupportedImageType { - return ([self contentType] ? YES : NO); -} - -@end