Skip to content

Commit d1db92d

Browse files
authored
Add deep links redaction (#798)
Ref: LIB-686
1 parent 633fc35 commit d1db92d

File tree

11 files changed

+175
-7
lines changed

11 files changed

+175
-7
lines changed

Analytics/Classes/Internal/SEGUtils.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@
1212
+ (NSData *_Nullable)dataFromPlist:(nonnull id)plist;
1313
+ (id _Nullable)plistFromData:(NSData *_Nonnull)data;
1414

15+
+ (id _Nullable)traverseJSON:(id _Nullable)object andReplaceWithFilters:(nonnull NSDictionary<NSString*, NSString*>*)patterns;
16+
1517
@end

Analytics/Classes/Internal/SEGUtils.m

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,62 @@ + (id _Nullable)plistFromData:(NSData *_Nonnull)data
3434
return plist;
3535
}
3636

37+
38+
+(id)traverseJSON:(id)object andReplaceWithFilters:(NSDictionary<NSString*, NSString*>*)patterns
39+
{
40+
if (object == nil || object == NSNull.null || [object isKindOfClass:NSNull.class]) {
41+
return object;
42+
}
43+
44+
if ([object isKindOfClass:NSDictionary.class]) {
45+
NSDictionary* dict = object;
46+
NSMutableDictionary* newDict = [NSMutableDictionary dictionaryWithCapacity:dict.count];
47+
48+
for (NSString* key in dict.allKeys) {
49+
newDict[key] = [self traverseJSON:dict[key] andReplaceWithFilters:patterns];
50+
}
51+
52+
return newDict;
53+
}
54+
55+
if ([object isKindOfClass:NSArray.class]) {
56+
NSArray* array = object;
57+
NSMutableArray* newArray = [NSMutableArray arrayWithCapacity:array.count];
58+
59+
for (int i = 0; i < array.count; i++) {
60+
newArray[i] = [self traverseJSON:array[i] andReplaceWithFilters:patterns];
61+
}
62+
63+
return newArray;
64+
}
65+
66+
if ([object isKindOfClass:NSString.class]) {
67+
NSError* error = nil;
68+
NSMutableString* str = [object mutableCopy];
69+
70+
for (NSString* pattern in patterns) {
71+
NSRegularExpression* re = [NSRegularExpression regularExpressionWithPattern:pattern
72+
options:0
73+
error:&error];
74+
75+
if (error) {
76+
@throw error;
77+
}
78+
79+
NSInteger matches = [re replaceMatchesInString:str
80+
options:0
81+
range:NSMakeRange(0, str.length)
82+
withTemplate:patterns[pattern]];
83+
84+
if (matches > 0) {
85+
SEGLog(@"%@ Redacted value from action: %@", self, pattern);
86+
}
87+
}
88+
89+
return str;
90+
}
91+
92+
return object;
93+
}
94+
3795
@end

Analytics/Classes/SEGAnalytics.m

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#import "SEGMiddleware.h"
1515
#import "SEGContext.h"
1616
#import "SEGIntegrationsManager.h"
17+
#import "Internal/SEGUtils.h"
1718

1819
static SEGAnalytics *__sharedInstance = nil;
1920

@@ -344,14 +345,17 @@ - (void)continueUserActivity:(NSUserActivity *)activity
344345
[properties addEntriesFromDictionary:activity.userInfo];
345346
properties[@"url"] = activity.webpageURL.absoluteString;
346347
properties[@"title"] = activity.title ?: @"";
348+
properties = [SEGUtils traverseJSON:properties
349+
andReplaceWithFilters:self.configuration.payloadFilters];
347350
[self track:@"Deep Link Opened" properties:[properties copy]];
348351
}
349352
}
350353

351354
- (void)openURL:(NSURL *)url options:(NSDictionary *)options
352355
{
353356
SEGOpenURLPayload *payload = [[SEGOpenURLPayload alloc] init];
354-
payload.url = url;
357+
payload.url = [NSURL URLWithString:[SEGUtils traverseJSON:url.absoluteString
358+
andReplaceWithFilters:self.configuration.payloadFilters]];
355359
payload.options = options;
356360
[self run:SEGEventTypeOpenURL payload:payload];
357361

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

Analytics/Classes/SEGAnalyticsConfiguration.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,30 @@ typedef NSMutableURLRequest *_Nonnull (^SEGRequestFactory)(NSURL *_Nonnull);
140140
*/
141141
@property (nonatomic, strong, nullable) id<SEGApplicationProtocol> application;
142142

143+
/**
144+
* A dictionary of filters to redact payloads before they are sent.
145+
* This is an experimental feature that currently only applies to Deep Links.
146+
* It is subject to change to allow for more flexible customizations in the future.
147+
*
148+
* The key of this dictionary should be a regular expression string pattern,
149+
* and the value should be a regular expression substitution template.
150+
*
151+
* By default, this contains a Facebook auth token filter, configured as such:
152+
* @code
153+
* @"(fb\\d+://authorize#access_token=)([^ ]+)": @"$1((redacted/fb-auth-token))"
154+
* @endcode
155+
*
156+
* This will replace any matching occurences to a redacted version:
157+
* @code
158+
* "fb123456789://authorize#access_token=secretsecretsecretsecret&some=data"
159+
* @endcode
160+
*
161+
* Becomes:
162+
* @code
163+
* "fb123456789://authorize#access_token=((redacted/fb-auth-token))"
164+
* @endcode
165+
*
166+
*/
167+
@property (nonatomic, strong, nonnull) NSDictionary<NSString*, NSString*>* payloadFilters;
168+
143169
@end

Analytics/Classes/SEGAnalyticsConfiguration.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ - (instancetype)init
5757
self.flushAt = 20;
5858
self.flushInterval = 30;
5959
self.maxQueueSize = 1000;
60+
self.payloadFilters = @{
61+
@"(fb\\d+://authorize#access_token=)([^ ]+)": @"$1((redacted/fb-auth-token))"
62+
};
6063
_factories = [NSMutableArray array];
6164
Class applicationClass = NSClassFromString(@"UIApplication");
6265
if (applicationClass) {

AnalyticsTests/AnalyticsTests-Bridging-Header.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#import <Analytics/UIViewController+SEGScreen.h>
1212
#import <Analytics/SEGAnalyticsUtils.h>
1313
#import <Analytics/SEGIntegrationsManager.h>
14+
#import <Analytics/SEGUtils.h>
1415

1516
#import "NSData+SEGGUNZIPP.h"
1617
// Temp hack. We should fix the LSNocilla podspec to make this header publicly available

AnalyticsTests/AnalyticsTests.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,29 @@ class AnalyticsTests: QuickSpec {
170170
expect(timer).toNot(beNil())
171171
expect(timer?.timeInterval) == config.flushInterval
172172
}
173+
174+
it("redacts sensible URLs from deep links tracking") {
175+
testMiddleware.swallowEvent = true
176+
analytics.configuration.trackDeepLinks = true
177+
analytics.open(URL(string: "fb123456789://authorize#access_token=hastoberedacted")!, options: [:])
178+
179+
180+
let event = testMiddleware.lastContext?.payload as? SEGTrackPayload
181+
expect(event?.event) == "Deep Link Opened"
182+
expect(event?.properties?["url"] as? String) == "fb123456789://authorize#access_token=((redacted/fb-auth-token))"
183+
}
184+
185+
it("redacts sensible URLs from deep links tracking using custom filters") {
186+
testMiddleware.swallowEvent = true
187+
analytics.configuration.payloadFilters["(myapp://auth\\?token=)([^&]+)"] = "$1((redacted/my-auth))"
188+
analytics.configuration.trackDeepLinks = true
189+
analytics.open(URL(string: "myapp://auth?token=hastoberedacted&other=stuff")!, options: [:])
190+
191+
192+
let event = testMiddleware.lastContext?.payload as? SEGTrackPayload
193+
expect(event?.event) == "Deep Link Opened"
194+
expect(event?.properties?["url"] as? String) == "myapp://auth?token=((redacted/my-auth))&other=stuff"
195+
}
173196
}
174197

175198
}

AnalyticsTests/AnalyticsUtilTests.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,54 @@ class AnalyticsUtilTests: QuickSpec {
6262
expect(queue) == [1, 2, 3, 4, 5]
6363
}
6464
})
65+
66+
describe("JSON traverse", {
67+
let filters = [
68+
"(foo)": "$1-bar"
69+
]
70+
71+
func equals(a: Any, b: Any) -> Bool {
72+
let aData = try! JSONSerialization.data(withJSONObject: a, options: .prettyPrinted) as NSData
73+
let bData = try! JSONSerialization.data(withJSONObject: b, options: .prettyPrinted)
74+
75+
return aData.isEqual(to: bData)
76+
}
77+
78+
it("works with strings") {
79+
expect(SEGUtils.traverseJSON("a b foo c", andReplaceWithFilters: filters) as? String) == "a b foo-bar c"
80+
}
81+
82+
it("works recursively") {
83+
expect(SEGUtils.traverseJSON("a b foo foo c", andReplaceWithFilters: filters) as? String) == "a b foo-bar foo-bar c"
84+
}
85+
86+
it("works with nested dictionaries") {
87+
let data = [
88+
"foo": [1, nil, "qfoob", ["baz": "foo"]],
89+
"bar": "foo"
90+
] as [String : Any]
91+
let input = SEGUtils.traverseJSON(data, andReplaceWithFilters: filters)
92+
let output = [
93+
"foo": [1, nil, "qfoo-barb", ["baz": "foo-bar"]],
94+
"bar": "foo-bar"
95+
] as [String : Any]
96+
97+
expect(equals(a: input!, b: output)) == true
98+
}
99+
100+
it("works with nested arrays") {
101+
let data = [
102+
[1, nil, "qfoob", ["baz": "foo"]],
103+
"foo"
104+
] as [Any]
105+
let input = SEGUtils.traverseJSON(data, andReplaceWithFilters: filters)
106+
let output = [
107+
[1, nil, "qfoo-barb", ["baz": "foo-bar"]],
108+
"foo-bar"
109+
] as [Any]
110+
111+
expect(equals(a: input!, b: output)) == true
112+
}
113+
})
65114
}
66115
}

AnalyticsTests/Utils/TestUtils.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ extension SEGSegmentIntegration {
6767
func test_queue() -> [AnyObject]? {
6868
return self.value(forKey: "queue") as? [AnyObject]
6969
}
70-
func test_dispatchBackground(block: @convention(block) () -> Void) {
70+
func test_dispatchBackground(block: @escaping @convention(block) () -> Void) {
7171
self.perform(Selector(("dispatchBackground:")), with: block)
7272
}
7373
}

Podfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ target 'AnalyticsTests' do
44
use_frameworks!
55

66
pod 'Quick', '~> 1.2.0'
7-
pod 'Nimble', '~> 7.0.3'
7+
pod 'Nimble', '~> 7.3.1'
88
pod 'Nocilla', '~> 0.11.0'
99
pod 'Alamofire', '~> 4.5'
1010
pod 'Alamofire-Synchronous', '~> 4.0'

Podfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ PODS:
22
- Alamofire (4.6.0)
33
- Alamofire-Synchronous (4.0.0):
44
- Alamofire (~> 4.0)
5-
- Nimble (7.0.3)
5+
- Nimble (7.3.1)
66
- Nocilla (0.11.0)
77
- Quick (1.2.0)
88
- SwiftTryCatch (1.0.0)
99

1010
DEPENDENCIES:
1111
- Alamofire (~> 4.5)
1212
- Alamofire-Synchronous (~> 4.0)
13-
- Nimble (~> 7.0.3)
13+
- Nimble (~> 7.3.1)
1414
- Nocilla (~> 0.11.0)
1515
- Quick (~> 1.2.0)
1616
- SwiftTryCatch (from `https://github.com/segmentio/SwiftTryCatch.git`)
@@ -35,11 +35,11 @@ CHECKOUT OPTIONS:
3535
SPEC CHECKSUMS:
3636
Alamofire: f41a599bd63041760b26d393ec1069d9d7b917f4
3737
Alamofire-Synchronous: eedf1e6e961c3795a63c74990b3f7d9fbfac7e50
38-
Nimble: 7f5a9c447a33002645a071bddafbfb24ea70e0ac
38+
Nimble: 04f732da099ea4d153122aec8c2a88fd0c7219ae
3939
Nocilla: 7af7a386071150cc8aa5da4da97d060f049dd61c
4040
Quick: 58d203b1c5e27fff7229c4c1ae445ad7069a7a08
4141
SwiftTryCatch: 2f4ef36cf5396bdb450006b70633dbce5260d3b3
4242

43-
PODFILE CHECKSUM: 70caa6b2011c61348e6dbbb35d12b64fa7558374
43+
PODFILE CHECKSUM: cf4abb4263c7b514d71c70514284ac657d90865d
4444

4545
COCOAPODS: 1.5.3

0 commit comments

Comments
 (0)