Skip to content

Commit f38221c

Browse files
authored
sync: enforce HTTP for loopback addresses only (#921)
Fixes SNT-386
1 parent 001bd8b commit f38221c

5 files changed

Lines changed: 94 additions & 2 deletions

File tree

Source/common/SNTConfigurator.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,12 @@ extern NSString* _Nonnull const kEnableMenuItemUserOverride;
704704
///
705705
@property(nullable, readonly, nonatomic) NSURL* syncBaseURL;
706706

707+
///
708+
/// Whether a SyncBaseURL value exists in the configuration, without any parsing or validation.
709+
/// Use this to distinguish "no URL configured" from "URL configured but rejected by policy."
710+
///
711+
@property(readonly, nonatomic) BOOL syncBaseURLConfigured;
712+
707713
///
708714
/// If enabled, syncing will use binary protobufs for transfer instead
709715
/// of JSON. Defaults to NO.

Source/common/SNTConfigurator.mm

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,10 @@ + (NSSet*)keyPathsForValuesAffectingSyncBaseURL {
566566
return [self configStateSet];
567567
}
568568

569+
+ (NSSet*)keyPathsForValuesAffectingSyncBaseURLConfigured {
570+
return [self configStateSet];
571+
}
572+
569573
+ (NSSet*)keyPathsForValuesAffectingSyncEnableProtoTransfer {
570574
return [self configStateSet];
571575
}
@@ -1124,9 +1128,27 @@ - (NSURL*)syncBaseURL {
11241128
urlString = [urlString stringByAppendingString:@"/"];
11251129
}
11261130
NSURL* url = [NSURL URLWithString:urlString];
1131+
1132+
// Only allow HTTP for loopback addresses. Everything else must use HTTPS.
1133+
if ([[url.scheme lowercaseString] isEqualToString:@"http"]) {
1134+
NSString* host = [url.host lowercaseString];
1135+
if (![host isEqualToString:@"localhost"] && ![host isEqualToString:@"127.0.0.1"] &&
1136+
![host isEqualToString:@"::1"]) {
1137+
LOGW(@"Rejecting non-localhost HTTP SyncBaseURL: %@", urlString);
1138+
return nil;
1139+
}
1140+
}
1141+
11271142
return url;
11281143
}
11291144

1145+
// Raw check: returns YES if a SyncBaseURL string is present in the config, without any
1146+
// URL parsing or validation. This lets callers distinguish "not configured" from
1147+
// "configured but rejected by syncBaseURL policy" (e.g. non-localhost HTTP).
1148+
- (BOOL)syncBaseURLConfigured {
1149+
return [self.configState[kSyncBaseURLKey] length] > 0;
1150+
}
1151+
11301152
- (BOOL)syncEnableProtoTransfer {
11311153
NSNumber* number = self.configState[kSyncEnableProtoTransfer];
11321154
return number ? [number boolValue] : NO;

Source/common/SNTConfiguratorTest.mm

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,64 @@ - (void)testInitMigratesRemovableMediaSyncStateKeys {
165165
}];
166166
}
167167

168+
- (void)testSyncBaseURLRejectsNonLocalhostHTTP {
169+
SNTConfigurator* sut = [[SNTConfigurator alloc] init];
170+
171+
// HTTPS is always allowed.
172+
sut.configState[@"SyncBaseURL"] = @"https://example.com/api";
173+
XCTAssertNotNil(sut.syncBaseURL);
174+
XCTAssertEqualObjects(sut.syncBaseURL.host, @"example.com");
175+
176+
// HTTP to localhost is allowed.
177+
sut.configState[@"SyncBaseURL"] = @"http://localhost:8080/api";
178+
XCTAssertNotNil(sut.syncBaseURL);
179+
XCTAssertEqualObjects(sut.syncBaseURL.host, @"localhost");
180+
181+
// HTTP to 127.0.0.1 is allowed.
182+
sut.configState[@"SyncBaseURL"] = @"http://127.0.0.1:8080/api";
183+
XCTAssertNotNil(sut.syncBaseURL);
184+
XCTAssertEqualObjects(sut.syncBaseURL.host, @"127.0.0.1");
185+
186+
// HTTP to ::1 is allowed.
187+
sut.configState[@"SyncBaseURL"] = @"http://[::1]:8080/api";
188+
XCTAssertNotNil(sut.syncBaseURL);
189+
XCTAssertEqualObjects(sut.syncBaseURL.host, @"::1");
190+
191+
// HTTP to a non-localhost host is rejected.
192+
sut.configState[@"SyncBaseURL"] = @"http://example.com/api";
193+
XCTAssertNil(sut.syncBaseURL);
194+
195+
sut.configState[@"SyncBaseURL"] = @"http://10.0.0.1/api";
196+
XCTAssertNil(sut.syncBaseURL);
197+
198+
// Empty and missing values return nil.
199+
sut.configState[@"SyncBaseURL"] = @"";
200+
XCTAssertNil(sut.syncBaseURL);
201+
202+
sut.configState[@"SyncBaseURL"] = nil;
203+
XCTAssertNil(sut.syncBaseURL);
204+
}
205+
206+
- (void)testSyncBaseURLConfigured {
207+
SNTConfigurator* sut = [[SNTConfigurator alloc] init];
208+
209+
// A value is configured, even if syncBaseURL rejects it.
210+
sut.configState[@"SyncBaseURL"] = @"http://example.com/api";
211+
XCTAssertNil(sut.syncBaseURL);
212+
XCTAssertTrue(sut.syncBaseURLConfigured);
213+
214+
// A valid value is also reported as configured.
215+
sut.configState[@"SyncBaseURL"] = @"https://example.com/api";
216+
XCTAssertTrue(sut.syncBaseURLConfigured);
217+
218+
// Empty and missing values are not configured.
219+
sut.configState[@"SyncBaseURL"] = @"";
220+
XCTAssertFalse(sut.syncBaseURLConfigured);
221+
222+
sut.configState[@"SyncBaseURL"] = nil;
223+
XCTAssertFalse(sut.syncBaseURLConfigured);
224+
}
225+
168226
- (void)testTelemetryFilterExpressions {
169227
SNTConfigurator* sut = [[SNTConfigurator alloc] init];
170228

Source/santactl/Commands/SNTCommandDoctor.mm

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,16 @@ - (BOOL)validateSync {
199199

200200
NSURL* syncBaseURL = config.syncBaseURL;
201201
if (!syncBaseURL) {
202+
if (config.syncBaseURLConfigured) {
203+
print(@"[-] SyncBaseURL is configured but was rejected "
204+
@"(HTTP is only allowed for localhost)");
205+
print(@"");
206+
return YES;
207+
}
202208
print(@"[+] Sync is disabled");
203209
print(@"");
204210
// Don't treat this as an error.
205-
return YES;
211+
return NO;
206212
}
207213
print(@"[+] Sync is enabled");
208214
print(@"[+] Machine ID: %s", config.machineID.UTF8String ?: "(not set)");

docs/src/lib/santaconfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -996,7 +996,7 @@ thousand static rules working correctly, but we don't recommend using StaticRule
996996
sync: [
997997
{
998998
key: "SyncBaseURL",
999-
description: `The base URL of the sync server`,
999+
description: `The base URL of the sync server. HTTPS is required unless the host is localhost (localhost, 127.0.0.1, or ::1).`,
10001000
type: "string",
10011001
},
10021002
{

0 commit comments

Comments
 (0)