Skip to content

Commit

Permalink
Add deep links redaction (segmentio#798)
Browse files Browse the repository at this point in the history
Ref: LIB-686
  • Loading branch information
fathyb authored and Sergei Shatunov committed Aug 15, 2019
1 parent 0cca7dd commit 517ecd1
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 7 deletions.
2 changes: 2 additions & 0 deletions Analytics/Classes/Internal/SEGUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@
+ (NSData *_Nullable)dataFromPlist:(nonnull id)plist;
+ (id _Nullable)plistFromData:(NSData *_Nonnull)data;

+ (id _Nullable)traverseJSON:(id _Nullable)object andReplaceWithFilters:(nonnull NSDictionary<NSString*, NSString*>*)patterns;

@end
58 changes: 58 additions & 0 deletions Analytics/Classes/Internal/SEGUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,62 @@ + (id _Nullable)plistFromData:(NSData *_Nonnull)data
return plist;
}


+(id)traverseJSON:(id)object andReplaceWithFilters:(NSDictionary<NSString*, NSString*>*)patterns
{
if (object == nil || object == NSNull.null || [object isKindOfClass:NSNull.class]) {
return object;
}

if ([object isKindOfClass:NSDictionary.class]) {
NSDictionary* dict = object;
NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:dict.count];

for (NSString* key in dict.allKeys) {
newDict[key] = [self traverseJSON:dict[key] andReplaceWithFilters:patterns];
}

return newDict;
}

if ([object isKindOfClass:NSArray.class]) {
NSArray* array = object;
NSMutableArray* newArray = [NSMutableArray arrayWithCapacity:array.count];

for (int i = 0; i < array.count; i++) {
newArray[i] = [self traverseJSON:array[i] andReplaceWithFilters:patterns];
}

return newArray;
}

if ([object isKindOfClass:NSString.class]) {
NSError* error = nil;
NSMutableString* str = [object mutableCopy];

for (NSString* pattern in patterns) {
NSRegularExpression* re = [NSRegularExpression regularExpressionWithPattern:pattern
options:0
error:&error];

if (error) {
@throw error;
}

NSInteger matches = [re replaceMatchesInString:str
options:0
range:NSMakeRange(0, str.length)
withTemplate:patterns[pattern]];

if (matches > 0) {
SEGLog(@"%@ Redacted value from action: %@", self, pattern);
}
}

return str;
}

return object;
}

@end
8 changes: 7 additions & 1 deletion Analytics/Classes/SEGAnalytics.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#import "SEGMiddleware.h"
#import "SEGContext.h"
#import "SEGIntegrationsManager.h"
#import "Internal/SEGUtils.h"

static SEGAnalytics *__sharedInstance = nil;

Expand Down Expand Up @@ -344,14 +345,17 @@ - (void)continueUserActivity:(NSUserActivity *)activity
[properties addEntriesFromDictionary:activity.userInfo];
properties[@"url"] = activity.webpageURL.absoluteString;
properties[@"title"] = activity.title ?: @"";
properties = [SEGUtils traverseJSON:properties
andReplaceWithFilters:self.configuration.payloadFilters];
[self track:@"Deep Link Opened" properties:[properties copy]];
}
}

- (void)openURL:(NSURL *)url options:(NSDictionary *)options
{
SEGOpenURLPayload *payload = [[SEGOpenURLPayload alloc] init];
payload.url = url;
payload.url = [NSURL URLWithString:[SEGUtils traverseJSON:url.absoluteString
andReplaceWithFilters:self.configuration.payloadFilters]];
payload.options = options;
[self run:SEGEventTypeOpenURL payload:payload];

Expand All @@ -362,6 +366,8 @@ - (void)openURL:(NSURL *)url options:(NSDictionary *)options
NSMutableDictionary *properties = [NSMutableDictionary dictionaryWithCapacity:options.count + 2];
[properties addEntriesFromDictionary:options];
properties[@"url"] = url.absoluteString;
properties = [SEGUtils traverseJSON:properties
andReplaceWithFilters:self.configuration.payloadFilters];
[self track:@"Deep Link Opened" properties:[properties copy]];
}

Expand Down
26 changes: 26 additions & 0 deletions Analytics/Classes/SEGAnalyticsConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,30 @@ typedef NSMutableURLRequest *_Nonnull (^SEGRequestFactory)(NSURL *_Nonnull);
*/
@property (nonatomic, strong, nullable) id<SEGApplicationProtocol> application;

/**
* A dictionary of filters to redact payloads before they are sent.
* This is an experimental feature that currently only applies to Deep Links.
* It is subject to change to allow for more flexible customizations in the future.
*
* The key of this dictionary should be a regular expression string pattern,
* and the value should be a regular expression substitution template.
*
* By default, this contains a Facebook auth token filter, configured as such:
* @code
* @"(fb\\d+://authorize#access_token=)([^ ]+)": @"$1((redacted/fb-auth-token))"
* @endcode
*
* This will replace any matching occurences to a redacted version:
* @code
* "fb123456789://authorize#access_token=secretsecretsecretsecret&some=data"
* @endcode
*
* Becomes:
* @code
* "fb123456789://authorize#access_token=((redacted/fb-auth-token))"
* @endcode
*
*/
@property (nonatomic, strong, nonnull) NSDictionary<NSString*, NSString*>* payloadFilters;

@end
3 changes: 3 additions & 0 deletions Analytics/Classes/SEGAnalyticsConfiguration.m
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ - (instancetype)init
self.flushAt = 20;
self.flushInterval = 30;
self.maxQueueSize = 1000;
self.payloadFilters = @{
@"(fb\\d+://authorize#access_token=)([^ ]+)": @"$1((redacted/fb-auth-token))"
};
_factories = [NSMutableArray array];
Class applicationClass = NSClassFromString(@"UIApplication");
if (applicationClass) {
Expand Down
1 change: 1 addition & 0 deletions AnalyticsTests/AnalyticsTests-Bridging-Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#import <Analytics/UIViewController+SEGScreen.h>
#import <Analytics/SEGAnalyticsUtils.h>
#import <Analytics/SEGIntegrationsManager.h>
#import <Analytics/SEGUtils.h>

#import "NSData+SEGGUNZIPP.h"
// Temp hack. We should fix the LSNocilla podspec to make this header publicly available
Expand Down
23 changes: 23 additions & 0 deletions AnalyticsTests/AnalyticsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,29 @@ class AnalyticsTests: QuickSpec {
expect(timer).toNot(beNil())
expect(timer?.timeInterval) == config.flushInterval
}

it("redacts sensible URLs from deep links tracking") {
testMiddleware.swallowEvent = true
analytics.configuration.trackDeepLinks = true
analytics.open(URL(string: "fb123456789://authorize#access_token=hastoberedacted")!, options: [:])


let event = testMiddleware.lastContext?.payload as? SEGTrackPayload
expect(event?.event) == "Deep Link Opened"
expect(event?.properties?["url"] as? String) == "fb123456789://authorize#access_token=((redacted/fb-auth-token))"
}

it("redacts sensible URLs from deep links tracking using custom filters") {
testMiddleware.swallowEvent = true
analytics.configuration.payloadFilters["(myapp://auth\\?token=)([^&]+)"] = "$1((redacted/my-auth))"
analytics.configuration.trackDeepLinks = true
analytics.open(URL(string: "myapp://auth?token=hastoberedacted&other=stuff")!, options: [:])


let event = testMiddleware.lastContext?.payload as? SEGTrackPayload
expect(event?.event) == "Deep Link Opened"
expect(event?.properties?["url"] as? String) == "myapp://auth?token=((redacted/my-auth))&other=stuff"
}
}

}
49 changes: 49 additions & 0 deletions AnalyticsTests/AnalyticsUtilTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,54 @@ class AnalyticsUtilTests: QuickSpec {
expect(queue) == [1, 2, 3, 4, 5]
}
})

describe("JSON traverse", {
let filters = [
"(foo)": "$1-bar"
]

func equals(a: Any, b: Any) -> Bool {
let aData = try! JSONSerialization.data(withJSONObject: a, options: .prettyPrinted) as NSData
let bData = try! JSONSerialization.data(withJSONObject: b, options: .prettyPrinted)

return aData.isEqual(to: bData)
}

it("works with strings") {
expect(SEGUtils.traverseJSON("a b foo c", andReplaceWithFilters: filters) as? String) == "a b foo-bar c"
}

it("works recursively") {
expect(SEGUtils.traverseJSON("a b foo foo c", andReplaceWithFilters: filters) as? String) == "a b foo-bar foo-bar c"
}

it("works with nested dictionaries") {
let data = [
"foo": [1, nil, "qfoob", ["baz": "foo"]],
"bar": "foo"
] as [String : Any]
let input = SEGUtils.traverseJSON(data, andReplaceWithFilters: filters)
let output = [
"foo": [1, nil, "qfoo-barb", ["baz": "foo-bar"]],
"bar": "foo-bar"
] as [String : Any]

expect(equals(a: input!, b: output)) == true
}

it("works with nested arrays") {
let data = [
[1, nil, "qfoob", ["baz": "foo"]],
"foo"
] as [Any]
let input = SEGUtils.traverseJSON(data, andReplaceWithFilters: filters)
let output = [
[1, nil, "qfoo-barb", ["baz": "foo-bar"]],
"foo-bar"
] as [Any]

expect(equals(a: input!, b: output)) == true
}
})
}
}
2 changes: 1 addition & 1 deletion AnalyticsTests/Utils/TestUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ extension SEGSegmentIntegration {
func test_queue() -> [AnyObject]? {
return self.value(forKey: "queue") as? [AnyObject]
}
func test_dispatchBackground(block: @convention(block) () -> Void) {
func test_dispatchBackground(block: @escaping @convention(block) () -> Void) {
self.perform(Selector(("dispatchBackground:")), with: block)
}
}
Expand Down
2 changes: 1 addition & 1 deletion Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ target 'AnalyticsTests' do
use_frameworks!

pod 'Quick', '~> 1.2.0'
pod 'Nimble', '~> 7.0.3'
pod 'Nimble', '~> 7.3.1'
pod 'Nocilla', '~> 0.11.0'
pod 'Alamofire', '~> 4.5'
pod 'Alamofire-Synchronous', '~> 4.0'
Expand Down
8 changes: 4 additions & 4 deletions Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ PODS:
- Alamofire (4.6.0)
- Alamofire-Synchronous (4.0.0):
- Alamofire (~> 4.0)
- Nimble (7.0.3)
- Nimble (7.3.1)
- Nocilla (0.11.0)
- Quick (1.2.0)
- SwiftTryCatch (1.0.0)

DEPENDENCIES:
- Alamofire (~> 4.5)
- Alamofire-Synchronous (~> 4.0)
- Nimble (~> 7.0.3)
- Nimble (~> 7.3.1)
- Nocilla (~> 0.11.0)
- Quick (~> 1.2.0)
- SwiftTryCatch (from `https://github.com/segmentio/SwiftTryCatch.git`)
Expand All @@ -35,11 +35,11 @@ CHECKOUT OPTIONS:
SPEC CHECKSUMS:
Alamofire: f41a599bd63041760b26d393ec1069d9d7b917f4
Alamofire-Synchronous: eedf1e6e961c3795a63c74990b3f7d9fbfac7e50
Nimble: 7f5a9c447a33002645a071bddafbfb24ea70e0ac
Nimble: 04f732da099ea4d153122aec8c2a88fd0c7219ae
Nocilla: 7af7a386071150cc8aa5da4da97d060f049dd61c
Quick: 58d203b1c5e27fff7229c4c1ae445ad7069a7a08
SwiftTryCatch: 2f4ef36cf5396bdb450006b70633dbce5260d3b3

PODFILE CHECKSUM: 70caa6b2011c61348e6dbbb35d12b64fa7558374
PODFILE CHECKSUM: cf4abb4263c7b514d71c70514284ac657d90865d

COCOAPODS: 1.5.3

0 comments on commit 517ecd1

Please sign in to comment.