Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PLAT-8748] Truncate strings longer than maxStringValueLength #1449

Merged
merged 5 commits into from
Aug 8, 2022
Merged
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
10 changes: 10 additions & 0 deletions Bugsnag.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,10 @@
01E8765E256684E700F4B70A /* URLSessionMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 01E8765D256684E700F4B70A /* URLSessionMock.m */; };
01E8765F256684E700F4B70A /* URLSessionMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 01E8765D256684E700F4B70A /* URLSessionMock.m */; };
01E87660256684E700F4B70A /* URLSessionMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 01E8765D256684E700F4B70A /* URLSessionMock.m */; };
01F9FCB628929336005EDD8C /* BSGSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F9FCB528929336005EDD8C /* BSGSerializationTests.m */; };
01F9FCB728929336005EDD8C /* BSGSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F9FCB528929336005EDD8C /* BSGSerializationTests.m */; };
01F9FCB828929336005EDD8C /* BSGSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F9FCB528929336005EDD8C /* BSGSerializationTests.m */; };
01F9FCB928929336005EDD8C /* BSGSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 01F9FCB528929336005EDD8C /* BSGSerializationTests.m */; };
3A700A9424A63ABC0068CD1B /* BugsnagThread.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A700A8024A63A8E0068CD1B /* BugsnagThread.h */; settings = {ATTRIBUTES = (Public, ); }; };
3A700A9524A63AC50068CD1B /* BugsnagSession.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A700A8124A63A8E0068CD1B /* BugsnagSession.h */; settings = {ATTRIBUTES = (Public, ); }; };
3A700A9624A63AC60068CD1B /* BugsnagStackframe.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A700A8224A63A8E0068CD1B /* BugsnagStackframe.h */; settings = {ATTRIBUTES = (Public, ); }; };
Expand Down Expand Up @@ -1628,6 +1632,7 @@
01DE903B26CEAF9E00455213 /* BSGUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BSGUtilsTests.m; sourceTree = "<group>"; };
01E8765C256684E700F4B70A /* URLSessionMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = URLSessionMock.h; sourceTree = "<group>"; };
01E8765D256684E700F4B70A /* URLSessionMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = URLSessionMock.m; sourceTree = "<group>"; };
01F9FCB528929336005EDD8C /* BSGSerializationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BSGSerializationTests.m; sourceTree = "<group>"; };
3A700A8024A63A8E0068CD1B /* BugsnagThread.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagThread.h; sourceTree = "<group>"; };
3A700A8124A63A8E0068CD1B /* BugsnagSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagSession.h; sourceTree = "<group>"; };
3A700A8224A63A8E0068CD1B /* BugsnagStackframe.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BugsnagStackframe.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2033,6 +2038,7 @@
0163BF5825823D8D008DC28B /* BSGNotificationBreadcrumbsTests.m */,
008966C82486D43600DC48C2 /* BSGOutOfMemoryTests.m */,
0130DEF82880203A00E5953F /* BSGRunContextTests.m */,
01F9FCB528929336005EDD8C /* BSGSerializationTests.m */,
CB6419AA25A73E8C00613D25 /* BSGStorageMigratorTests.m */,
017DCF9A287422BB000ECB22 /* BSGTelemetryTests.m */,
01DE903B26CEAF9E00455213 /* BSGUtilsTests.m */,
Expand Down Expand Up @@ -3236,6 +3242,7 @@
CBA2249B251E429C00B87416 /* TestSupport.m in Sources */,
008967332486D43700DC48C2 /* BugsnagClientTests.m in Sources */,
004E353F2487B3BD007FBAE4 /* BugsnagSwiftConfigurationTests.swift in Sources */,
01F9FCB628929336005EDD8C /* BSGSerializationTests.m in Sources */,
008967542486D43700DC48C2 /* BugsnagOnCrashTest.m in Sources */,
008967152486D43700DC48C2 /* BugsnagCollectionsTests.m in Sources */,
01E8765E256684E700F4B70A /* URLSessionMock.m in Sources */,
Expand Down Expand Up @@ -3395,6 +3402,7 @@
0089671C2486D43700DC48C2 /* BugsnagSessionTest.m in Sources */,
008967AC2486D43700DC48C2 /* BSG_KSMachTests.m in Sources */,
0163BF5A25823D8D008DC28B /* BSGNotificationBreadcrumbsTests.m in Sources */,
01F9FCB728929336005EDD8C /* BSGSerializationTests.m in Sources */,
01BDB1FD25DEBFB300A91FAF /* BSGEventUploadKSCrashReportOperationTests.m in Sources */,
00896A452486DBF000DC48C2 /* BugsnagConfigurationTests.m in Sources */,
008967492486D43700DC48C2 /* BugsnagUserTest.m in Sources */,
Expand Down Expand Up @@ -3543,6 +3551,7 @@
E701FAAD2490EFD9008D842F /* EventApiValidationTest.m in Sources */,
008967472486D43700DC48C2 /* BugsnagTests.m in Sources */,
010993A6273D188B00128BBE /* BSGFeatureFlagStoreTests.m in Sources */,
01F9FCB828929336005EDD8C /* BSGSerializationTests.m in Sources */,
008967A72486D43700DC48C2 /* KSString_Tests.m in Sources */,
0089671A2486D43700DC48C2 /* BugsnagErrorTest.m in Sources */,
008967172486D43700DC48C2 /* BugsnagCollectionsTests.m in Sources */,
Expand Down Expand Up @@ -3843,6 +3852,7 @@
CB28F0A228294D4F003AB200 /* KSCrashReportWriterTests.m in Sources */,
CB28F0D3282A4B91003AB200 /* BugsnagErrorTest.m in Sources */,
CB28F0DE282A4BEE003AB200 /* BugsnagSessionTrackerStopTest.m in Sources */,
01F9FCB928929336005EDD8C /* BSGSerializationTests.m in Sources */,
CB28F0B328294DD0003AB200 /* BSGClientObserverTests.m in Sources */,
CB28F0B128294D4F003AB200 /* KSCrashSentry_Tests.m in Sources */,
CB28F0B228294D52003AB200 /* XCTestCase+KSCrash.m in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion Bugsnag/Breadcrumbs/BugsnagBreadcrumbs.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithConfiguration:(BugsnagConfiguration *)config;

/**
* The breadcrumbs stored in memory.
* Returns an array of new objects representing the breadcrumbs stored in memory.
*/
@property (readonly, nonatomic) NSArray<BugsnagBreadcrumb *> *breadcrumbs;

Expand Down
2 changes: 2 additions & 0 deletions Bugsnag/Configuration/BugsnagConfiguration.m
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ - (nonnull id)copyWithZone:(nullable NSZone *)zone {
[copy setSendLaunchCrashesSynchronously:self.sendLaunchCrashesSynchronously];
[copy setMaxPersistedEvents:self.maxPersistedEvents];
[copy setMaxPersistedSessions:self.maxPersistedSessions];
[copy setMaxStringValueLength:self.maxStringValueLength];
[copy setMaxBreadcrumbs:self.maxBreadcrumbs];
[copy setNotifier:self.notifier];
[copy setFeatureFlagStore:self.featureFlagStore];
Expand Down Expand Up @@ -189,6 +190,7 @@ - (instancetype)initWithApiKey:(NSString *)apiKey {
_maxBreadcrumbs = 50;
_maxPersistedEvents = 32;
_maxPersistedSessions = 128;
_maxStringValueLength = 10000;
_autoTrackSessions = YES;
#if BSG_HAVE_MACH_THREADS
_sendThreads = BSGThreadSendPolicyAlways;
Expand Down
4 changes: 4 additions & 0 deletions Bugsnag/Delivery/BSGEventUploadOperation.m
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,13 @@ - (void)runWithDelegate:(id<BSGEventUploadOperationDelegate>)delegate completion

NSDictionary *eventPayload;
@try {
[event truncateStrings:configuration.maxStringValueLength];
eventPayload = [event toJsonWithRedactedKeys:configuration.redactedKeys];
} @catch (NSException *exception) {
bsg_log_err(@"Discarding event %@ because an exception was thrown by -toJsonWithRedactedKeys: %@", self.name, exception);
[BSGInternalErrorReporter.sharedInstance reportException:exception diagnostics:nil groupingHash:
[NSString stringWithFormat:@"BSGEventUploadOperation -[runWithDelegate:completionHandler:] %@ %@",
exception.name, exception.reason]];
[self deleteEvent];
completionHandler();
return;
Expand Down
38 changes: 15 additions & 23 deletions Bugsnag/Helpers/BSGSerialization.h
Original file line number Diff line number Diff line change
@@ -1,40 +1,32 @@
#ifndef BugsnagJSONSerializable_h
#define BugsnagJSONSerializable_h

#import <Foundation/Foundation.h>

/**
Removes any values which would be rejected by NSJSONSerialization for
documented reasons

@param input an array
@return a new array
*/
NSArray *_Nonnull BSGSanitizeArray(NSArray *_Nonnull input);
NS_ASSUME_NONNULL_BEGIN

/**
Removes any values which would be rejected by NSJSONSerialization for
documented reasons
documented reasons or is NSNull

@param input a dictionary
@return a new dictionary
*/
NSDictionary *_Nonnull BSGSanitizeDict(NSDictionary *_Nonnull input);

/**
Checks whether the base type would be accepted by the serialization process

@param obj any object or nil
@return YES if the object is an Array, Dictionary, String, Number, or NSNull
*/
BOOL BSGIsSanitizedType(id _Nullable obj);
NSMutableDictionary * BSGSanitizeDict(NSDictionary *input);

/**
Cleans the object, including nested dictionary and array values

@param obj any object or nil
@return a new object for serialization or nil if the obj was incompatible
@return a new object for serialization or nil if the obj was incompatible or NSNull
*/
id _Nullable BSGSanitizeObject(id _Nullable obj);

#endif
typedef struct _BSGTruncateContext {
NSUInteger maxLength;
NSUInteger strings;
NSUInteger length;
} BSGTruncateContext;

NSString * BSGTruncateString(BSGTruncateContext *context, NSString *string);

id BSGTruncateStrings(BSGTruncateContext *context, id object);

NS_ASSUME_NONNULL_END
71 changes: 45 additions & 26 deletions Bugsnag/Helpers/BSGSerialization.m
Original file line number Diff line number Diff line change
@@ -1,41 +1,26 @@
#import "BSGSerialization.h"
#import "BugsnagLogger.h"

#import "BSGJSONSerialization.h"
#import "BugsnagLogger.h"

BOOL BSGIsSanitizedType(id obj) {
static dispatch_once_t onceToken;
static NSArray *allowedTypes = nil;
dispatch_once(&onceToken, ^{
allowedTypes = @[
[NSArray class], [NSDictionary class], [NSNull class],
[NSNumber class], [NSString class]
];
});

for (Class klass in allowedTypes) {
if ([obj isKindOfClass:klass])
return YES;
}
return NO;
}
static NSArray * BSGSanitizeArray(NSArray *input);

id BSGSanitizeObject(id obj) {
if ([obj isKindOfClass:[NSNumber class]]) {
NSNumber *number = obj;
if (![number isEqualToNumber:[NSDecimalNumber notANumber]] &&
!isinf([number doubleValue]))
return obj;
} else if ([obj isKindOfClass:[NSArray class]]) {
if ([obj isKindOfClass:[NSArray class]]) {
return BSGSanitizeArray(obj);
} else if ([obj isKindOfClass:[NSDictionary class]]) {
return BSGSanitizeDict(obj);
} else if (BSGIsSanitizedType(obj)) {
} else if ([obj isKindOfClass:[NSString class]]) {
return obj;
} else if ([obj isKindOfClass:[NSNumber class]]
&& ![obj isEqualToNumber:[NSDecimalNumber notANumber]]
&& !isinf([obj doubleValue])) {
return obj;
}
return nil;
}

NSDictionary *_Nonnull BSGSanitizeDict(NSDictionary *input) {
NSMutableDictionary * BSGSanitizeDict(NSDictionary *input) {
__block NSMutableDictionary *output =
[NSMutableDictionary dictionaryWithCapacity:[input count]];
[input enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj,
Expand All @@ -49,7 +34,7 @@ id BSGSanitizeObject(id obj) {
return output;
}

NSArray *BSGSanitizeArray(NSArray *input) {
static NSArray * BSGSanitizeArray(NSArray *input) {
NSMutableArray *output = [NSMutableArray arrayWithCapacity:[input count]];
for (id obj in input) {
id cleanedObject = BSGSanitizeObject(obj);
Expand All @@ -58,3 +43,37 @@ id BSGSanitizeObject(id obj) {
}
return output;
}

NSString * BSGTruncateString(BSGTruncateContext *context, NSString *string) {
const NSUInteger inputLength = string.length;
if (inputLength <= context->maxLength) return string;
// Prevent chopping in the middle of a composed character sequence
NSRange range = [string rangeOfComposedCharacterSequenceAtIndex:context->maxLength];
NSString *output = [string substringToIndex:range.location];
NSUInteger count = inputLength - range.location;
context->strings++;
context->length += count;
return [output stringByAppendingFormat:@"\n***%lu CHARS TRUNCATED***", (unsigned long)count];
}

id BSGTruncateStrings(BSGTruncateContext *context, id object) {
if ([object isKindOfClass:[NSString class]]) {
return BSGTruncateString(context, object);
}
if ([object isKindOfClass:[NSDictionary class]]) {
NSMutableDictionary *output = [NSMutableDictionary dictionaryWithCapacity:((NSDictionary *)object).count];
for (NSString *key in (NSDictionary *)object) {
id value = ((NSDictionary *)object)[key];
output[key] = BSGTruncateStrings(context, value);
}
return output;
}
if ([object isKindOfClass:[NSArray class]]) {
NSMutableArray *output = [NSMutableArray arrayWithCapacity:((NSArray *)object).count];
for (id element in (NSArray *)object) {
[output addObject:BSGTruncateStrings(context, element)];
}
return output;
}
return object;
}
40 changes: 1 addition & 39 deletions Bugsnag/Metadata/BugsnagMetadata.m
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict {
if ((self = [super init])) {
// Ensure that the instantiating dictionary is mutable.
// Saves checks later.
_dictionary = [self sanitizeDictionary:dict];
_dictionary = BSGSanitizeDict(dict);
self.stateEventBlocks = [NSMutableArray new];
}
if (self.observer) {
Expand All @@ -65,44 +65,6 @@ - (instancetype)initWithDictionary:(NSDictionary *)dict {
return self;
}

/**
* Sanitizes the given dictionary to prevent [NSNull null] values from being added
* to the metadata when deserializing a payload.
*
* @param dictionary the input dictionary
* @return a sanitized dictionary
*/
- (NSMutableDictionary *)sanitizeDictionary:(NSDictionary *)dictionary {
NSMutableDictionary *input = [dictionary mutableCopy];

for (NSString *key in [input allKeys]) {
id obj = input[key];

if (obj == [NSNull null]) {
[input removeObjectForKey:key];
} else if ([obj isKindOfClass:[NSDictionary class]]) {
input[key] = [self sanitizeDictionary:obj];
} else if ([obj isKindOfClass:[NSArray class]]) {
input[key] = [self sanitizeArray:obj];
}
}
return input;
}

- (NSMutableArray *)sanitizeArray:(NSArray *)obj {
NSMutableArray *ary = [obj mutableCopy];
[ary removeObject:[NSNull null]];

for (NSUInteger k = 0; k < [ary count]; ++k) {
if ([ary[k] isKindOfClass:[NSDictionary class]]) {
ary[k] = [self sanitizeDictionary:ary[k]];
} else if ([ary[k] isKindOfClass:[NSArray class]]) {
ary[k] = [self sanitizeArray:ary[k]];
}
}
return ary;
}

- (NSDictionary *)toDictionary {
@synchronized (self) {
return [self.dictionary mutableCopy];
Expand Down
4 changes: 3 additions & 1 deletion Bugsnag/Payload/BugsnagEvent+Private.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ NS_ASSUME_NONNULL_BEGIN
/// An array of string representations of BSGErrorType describing the types of stackframe / stacktrace in this error.
@property (readonly, nonatomic) NSArray<NSString *> *stacktraceTypes;

/// Usage telemetry info, from BSGTelemetryCreateUsage()
/// Usage telemetry info, from BSGTelemetryCreateUsage(), or nil if BSGTelemetryUsage is not enabled.
@property (readwrite, nullable, nonatomic) NSDictionary *usage;

@property (readwrite, nonnull, nonatomic) BugsnagUser *user;
Expand Down Expand Up @@ -73,6 +73,8 @@ NS_ASSUME_NONNULL_BEGIN

- (NSDictionary *)toJsonWithRedactedKeys:(nullable NSSet *)redactedKeys;

- (void)truncateStrings:(NSUInteger)maxLength;

- (void)notifyUnhandledOverridden;

@end
Expand Down
28 changes: 27 additions & 1 deletion Bugsnag/Payload/BugsnagEvent.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#import "BugsnagEvent+Private.h"

#import "BSGDefines.h"
#import "BSGFeatureFlagStore.h"
#import "BSGKeys.h"
#import "BSGSerialization.h"
Expand All @@ -31,7 +32,6 @@
#import "BugsnagStacktrace.h"
#import "BugsnagThread+Private.h"
#import "BugsnagUser+Private.h"
#import "BSGDefines.h"

static NSString * const RedactedMetadataValue = @"[REDACTED]";

Expand Down Expand Up @@ -690,6 +690,32 @@ - (void)symbolicateIfNeeded {
}
}

- (void)truncateStrings:(NSUInteger)maxLength {
BSGTruncateContext context = {
.maxLength = maxLength
};

for (BugsnagBreadcrumb *breadcrumb in self.breadcrumbs) {
breadcrumb.message = BSGTruncateString(&context, breadcrumb.message);
breadcrumb.metadata = BSGTruncateStrings(&context, breadcrumb.metadata);
}

BugsnagMetadata *metadata = self.metadata;
if (metadata) {
self.metadata = [[BugsnagMetadata alloc] initWithDictionary:
BSGTruncateStrings(&context, metadata.dictionary)];
}

NSDictionary *usage = self.usage;
if (usage) {
self.usage = BSGDictMerge(@{
@"system": @{
@"stringCharsTruncated": @(context.length),
@"stringsTruncated": @(context.strings)}
}, usage);
}
}

- (BOOL)unhandled {
return self.handledState.unhandled;
}
Expand Down
9 changes: 9 additions & 0 deletions Bugsnag/include/Bugsnag/BugsnagConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,15 @@ BUGSNAG_EXTERN
*/
@property (nonatomic) NSUInteger maxBreadcrumbs;

/**
* The maximum length of breadcrumb messages and metadata string values.
*
* Values longer than this will be truncated prior to sending, after running any OnSendError blocks.
*
* The default value is 10000.
*/
@property (nonatomic) NSUInteger maxStringValueLength;

/**
* Whether User information should be persisted to disk between application runs.
* Defaults to True.
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Changelog

### Enhancements

* Truncate breadcrumb and metadata strings that are longer than `configuration.maxStringValueLength`.
[#1449](https://github.com/bugsnag/bugsnag-cocoa/pull/1449)

* Add `+[BugsnagStackframe stackframesWithCallStackReturnAddresses:]` to public headers.
[#1446](https://github.com/bugsnag/bugsnag-cocoa/pull/1446)

Expand Down
Loading