From 9351b15eb2bd73f3f93dbb25a740ac16321e3431 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Mon, 5 Oct 2026 11:46:27 -0500 Subject: [PATCH 1/5] fix: [SDK-5426] reject blank identity strings Empty strings were stored as aliases, tags, triggers, and app ids. Reject them before storage so iOS matches the other client SDKs. Co-authored-by: Cursor --- .../OneSignal.xcodeproj/project.pbxproj | 12 +++ .../OneSignalInAppMessages.m | 17 +++- .../OneSignalOSCore/Source/OSInputGuard.swift | 93 +++++++++++++++++++ .../OSInputGuardTests.swift | 59 ++++++++++++ .../Source/OneSignalUserManagerImpl.swift | 13 +++ .../InputGuardUserTests.swift | 78 ++++++++++++++++ iOS_SDK/OneSignalSDK/Source/OneSignal.m | 11 ++- 7 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSInputGuard.swift create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSInputGuardTests.swift create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 2b73ef84c9..892b17bc3c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -84,6 +84,8 @@ A5048F01A1B2C3D4E5F6000A /* OSFeatureFlagsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5048F01A1B2C3D4E5F60009 /* OSFeatureFlagsTests.swift */; }; A5048F01A1B2C3D4E5F6100B /* OSFeatureFlagsRefreshServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5048F01A1B2C3D4E5F6100A /* OSFeatureFlagsRefreshServiceTests.swift */; }; A5062F01A1B2C3D4E5F60002 /* OSDeviceGestureDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5062F01A1B2C3D4E5F60001 /* OSDeviceGestureDetector.swift */; }; + A5426001A1B2C3D4E5F60002 /* OSInputGuard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5426001A1B2C3D4E5F60001 /* OSInputGuard.swift */; }; + A5426001A1B2C3D4E5F60004 /* OSInputGuardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5426001A1B2C3D4E5F60003 /* OSInputGuardTests.swift */; }; A5062F01A1B2C3D4E5F60004 /* OSDeviceGestureDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5062F01A1B2C3D4E5F60003 /* OSDeviceGestureDetectorTests.swift */; }; C781A33FED62B4B54221A09A /* OSLogCrashHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B6A59620B83538CEFF77269 /* OSLogCrashHandlerTests.swift */; }; 32D3A6EA8AD44274B5CE378A /* FileLogStoreRetentionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CC252C94ECB485E8D0380E9 /* FileLogStoreRetentionTests.swift */; }; @@ -211,6 +213,7 @@ 3CC063E02B6D7F2A002BB07F /* OneSignalUserMocks.h in Headers */ = {isa = PBXBuildFile; fileRef = 3CC063DF2B6D7F2A002BB07F /* OneSignalUserMocks.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3CC063E62B6D7F96002BB07F /* OneSignalUserMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC063E52B6D7F96002BB07F /* OneSignalUserMocks.swift */; }; 3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */; }; + A5426001A1B2C3D4E5F60006 /* InputGuardUserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5426001A1B2C3D4E5F60005 /* InputGuardUserTests.swift */; }; 3CC063EF2B6D7FE8002BB07F /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; }; 3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */; }; 3CC9A6342AFA1FDE008F68FD /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3CC9A6332AFA1FDD008F68FD /* PrivacyInfo.xcprivacy */; }; @@ -1459,6 +1462,7 @@ 3CC063E52B6D7F96002BB07F /* OneSignalUserMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalUserMocks.swift; sourceTree = ""; }; 3CC063EB2B6D7FE8002BB07F /* OneSignalUserTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OneSignalUserTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalUserTests.swift; sourceTree = ""; }; + A5426001A1B2C3D4E5F60005 /* InputGuardUserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputGuardUserTests.swift; sourceTree = ""; }; 3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConcurrencyTests.swift; sourceTree = ""; }; 3CC9A6332AFA1FDD008F68FD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3CC9A6352AFA26E7008F68FD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; @@ -1827,6 +1831,8 @@ A5048F01A1B2C3D4E5F60009 /* OSFeatureFlagsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFeatureFlagsTests.swift; sourceTree = ""; }; A5048F01A1B2C3D4E5F6100A /* OSFeatureFlagsRefreshServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSFeatureFlagsRefreshServiceTests.swift; sourceTree = ""; }; A5062F01A1B2C3D4E5F60001 /* OSDeviceGestureDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSDeviceGestureDetector.swift; sourceTree = ""; }; + A5426001A1B2C3D4E5F60001 /* OSInputGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSInputGuard.swift; sourceTree = ""; }; + A5426001A1B2C3D4E5F60003 /* OSInputGuardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSInputGuardTests.swift; sourceTree = ""; }; A5062F01A1B2C3D4E5F60003 /* OSDeviceGestureDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSDeviceGestureDetectorTests.swift; sourceTree = ""; }; 3B6A59620B83538CEFF77269 /* OSLogCrashHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogCrashHandlerTests.swift; sourceTree = ""; }; 9CC252C94ECB485E8D0380E9 /* FileLogStoreRetentionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLogStoreRetentionTests.swift; sourceTree = ""; }; @@ -2322,6 +2328,7 @@ 3C448BA12936B474002F96BC /* OSBackgroundTaskManager.swift */, 4710EA522B8FCFB200435356 /* OSDispatchQueue.swift */, A5062F01A1B2C3D4E5F60001 /* OSDeviceGestureDetector.swift */, + A5426001A1B2C3D4E5F60001 /* OSInputGuard.swift */, DEFB3E642BB7346D00E65DAD /* OSLiveActivities.swift */, DEFB3E662BB735B500E65DAD /* OSStubLiveActivities.swift */, ); @@ -2456,6 +2463,7 @@ 3CDE664A2BFC2A55006DA114 /* OneSignalUserTests-Bridging-Header.h */, 3CF11E3E2C6D61AC002856F5 /* Executors */, 3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */, + A5426001A1B2C3D4E5F60005 /* InputGuardUserTests.swift */, 3CA93BC6300B0100000724B3 /* SubscriptionModelConcurrencyTests.swift */, 3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */, 3CB331672F281679000E1801 /* CustomEventsIntegrationTests.swift */, @@ -2628,6 +2636,7 @@ A5048F01A1B2C3D4E5F60009 /* OSFeatureFlagsTests.swift */, A5048F01A1B2C3D4E5F6100A /* OSFeatureFlagsRefreshServiceTests.swift */, A5062F01A1B2C3D4E5F60003 /* OSDeviceGestureDetectorTests.swift */, + A5426001A1B2C3D4E5F60003 /* OSInputGuardTests.swift */, 3B6A59620B83538CEFF77269 /* OSLogCrashHandlerTests.swift */, 9CC252C94ECB485E8D0380E9 /* FileLogStoreRetentionTests.swift */, 3C23A21A2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift */, @@ -4511,6 +4520,7 @@ A5048F01A1B2C3D4E5F6000D /* OSFeatureManagerImpl.swift in Sources */, A5048F01A1B2C3D4E5F60008 /* OSFeatureFlagsRefreshService.swift in Sources */, A5062F01A1B2C3D4E5F60002 /* OSDeviceGestureDetector.swift in Sources */, + A5426001A1B2C3D4E5F60002 /* OSInputGuard.swift in Sources */, 3C4F9E4428A4466C009F453A /* OSOperationRepo.swift in Sources */, 3C14E3B02FAE54C006ED053 /* IOSLogger.swift in Sources */, 698F58A488FCE503DFD5247F /* OSLogCrashHandler.swift in Sources */, @@ -4599,6 +4609,7 @@ 3CF11E3D2C6D6155002856F5 /* UserExecutorTests.swift in Sources */, 3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */, 3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */, + A5426001A1B2C3D4E5F60006 /* InputGuardUserTests.swift in Sources */, 3CA93BC7300B0100000724B3 /* SubscriptionModelConcurrencyTests.swift in Sources */, 3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */, 3CB3316A2F281692000E1801 /* OSCustomEventsExecutorTests.swift in Sources */, @@ -4683,6 +4694,7 @@ A5048F01A1B2C3D4E5F6000A /* OSFeatureFlagsTests.swift in Sources */, A5048F01A1B2C3D4E5F6100B /* OSFeatureFlagsRefreshServiceTests.swift in Sources */, A5062F01A1B2C3D4E5F60004 /* OSDeviceGestureDetectorTests.swift in Sources */, + A5426001A1B2C3D4E5F60004 /* OSInputGuardTests.swift in Sources */, C781A33FED62B4B54221A09A /* OSLogCrashHandlerTests.swift in Sources */, 32D3A6EA8AD44274B5CE378A /* FileLogStoreRetentionTests.swift in Sources */, 3C23A21B2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.m index f9540813cf..1e4da8eaf7 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.m @@ -28,6 +28,7 @@ of this software and associated documentation files (the "Software"), to deal #import "OneSignalInAppMessages.h" #import "OSMessagingController.h" #import "OSInAppMessageMigrationController.h" +#import @implementation OneSignalInAppMessages @@ -77,8 +78,10 @@ + (void)addTrigger:(NSString * _Nonnull)key withValue:(NSString * _Nonnull)value if ([OSPrivacyConsentController shouldLogMissingPrivacyConsentErrorWithMethodName:@"addTrigger:withValue:"]) return; - if (!key) { - [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:@"Attempted to set a trigger with a nil key."]; + if ([OSInputGuard isMissing:key api:@"addTrigger: key"]) + return; + if (!value) { + [OSInputGuard isMissing:nil api:@"addTrigger: value"]; return; } @@ -90,6 +93,9 @@ + (void)addTriggers:(NSDictionary * _Nonnull)triggers { if ([OSPrivacyConsentController shouldLogMissingPrivacyConsentErrorWithMethodName:@"addTriggers:"]) return; + if ([OSInputGuard hasMissingEntries:triggers api:@"addTriggers" allowEmptyValue:YES]) + return; + [OSMessagingController.sharedInstance addTriggers:triggers]; } @@ -98,10 +104,8 @@ + (void)removeTrigger:(NSString * _Nonnull)key { if ([OSPrivacyConsentController shouldLogMissingPrivacyConsentErrorWithMethodName:@"removeTriggerForKey:"]) return; - if (!key) { - [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:@"Attempted to remove a trigger with a nil key."]; + if ([OSInputGuard isMissing:key api:@"removeTrigger: key"]) return; - } [OSMessagingController.sharedInstance removeTriggersForKeys:@[key]]; } @@ -111,6 +115,9 @@ + (void)removeTriggers:(NSArray * _Nonnull)keys { if ([OSPrivacyConsentController shouldLogMissingPrivacyConsentErrorWithMethodName:@"removeTriggerForKey:"]) return; + if ([OSInputGuard isMissingAny:keys api:@"removeTriggers: key"]) + return; + [OSMessagingController.sharedInstance removeTriggersForKeys:keys]; } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSInputGuard.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSInputGuard.swift new file mode 100644 index 0000000000..50f0bd6284 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSInputGuard.swift @@ -0,0 +1,93 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import Foundation +import OneSignalCore + +@objc(OSInputGuard) +public final class OSInputGuard: NSObject { + @objc(isMissing:api:) + public static func isMissing(_ value: String?, _ api: String) -> Bool { + if let value, !value.isEmpty { + return false + } + OneSignalLog.onesignalLog(.LL_ERROR, message: "[OneSignal] \(api) is required") + return true + } + + @objc(isMissingAny:api:) + public static func isMissingAny(_ values: NSArray?, _ api: String) -> Bool { + guard let values else { + return isMissing(nil, api) + } + for item in values where isMissing(item as? String, api) { + return true + } + return false + } + + /// `allowEmptyValue` keeps "" and still rejects a null value. + @objc(hasMissingEntries:api:allowEmptyValue:) + public static func hasMissingEntries( + _ values: NSDictionary?, + _ api: String, + allowEmptyValue: Bool + ) -> Bool { + guard let values else { + return isMissing(nil, api) + } + for (key, item) in values { + if isMissing(key as? String, "\(api): key") { + return true + } + if allowEmptyValue { + if item is NSNull { + return isMissing(nil, "\(api): value") + } + continue + } + if isMissing(item as? String, "\(api): value") { + return true + } + } + return false + } + + @nonobjc + public static func isMissingAny(_ values: [String]?, _ api: String) -> Bool { + isMissingAny(values as NSArray?, api) + } + + @nonobjc + public static func hasMissingEntries( + _ values: [String: String]?, + _ api: String, + allowEmptyValue: Bool + ) -> Bool { + hasMissingEntries(values as NSDictionary?, api, allowEmptyValue: allowEmptyValue) + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSInputGuardTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSInputGuardTests.swift new file mode 100644 index 0000000000..cc730c634d --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSInputGuardTests.swift @@ -0,0 +1,59 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import XCTest +@testable import OneSignalOSCore + +final class OSInputGuardTests: XCTestCase { + func testIsMissingRejectsNilAndEmptyAndKeepsWhitespace() { + XCTAssertTrue(OSInputGuard.isMissing(nil, "login: externalId")) + XCTAssertTrue(OSInputGuard.isMissing("", "login: externalId")) + XCTAssertFalse(OSInputGuard.isMissing(" ", "login: externalId")) + XCTAssertFalse(OSInputGuard.isMissing("user", "login: externalId")) + } + + func testIsMissingAnyRejectsABlankEntryAndANilCollection() { + XCTAssertTrue(OSInputGuard.isMissingAny(nil as NSArray?, "removeTags: key")) + XCTAssertTrue(OSInputGuard.isMissingAny(["ok", ""], "removeTags: key")) + XCTAssertFalse(OSInputGuard.isMissingAny(["ok", " "], "removeTags: key")) + } + + func testHasMissingEntriesRejectsBlankKeysAndBlankAliasIds() { + XCTAssertTrue(OSInputGuard.hasMissingEntries(["": "id"], "addAliases", allowEmptyValue: false)) + XCTAssertTrue(OSInputGuard.hasMissingEntries(["label": ""], "addAliases", allowEmptyValue: false)) + XCTAssertFalse(OSInputGuard.hasMissingEntries(["label": "id"], "addAliases", allowEmptyValue: false)) + XCTAssertFalse(OSInputGuard.hasMissingEntries(["label": " "], "addAliases", allowEmptyValue: false)) + } + + func testHasMissingEntriesAllowsAnEmptyTagValueAndRejectsNull() { + XCTAssertFalse(OSInputGuard.hasMissingEntries(["key": ""], "addTags", allowEmptyValue: true)) + XCTAssertTrue(OSInputGuard.hasMissingEntries(["": "value"], "addTags", allowEmptyValue: true)) + let tags = NSMutableDictionary() + tags["key"] = NSNull() + XCTAssertTrue(OSInputGuard.hasMissingEntries(tags, "addTags", allowEmptyValue: true)) + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 4852a2b999..e3c34af6bb 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -720,6 +720,7 @@ extension OneSignalUserManagerImpl: OSUser { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "addAlias") else { return } + if OSInputGuard.isMissing(label, "addAlias: label") || OSInputGuard.isMissing(id, "addAlias: id") { return } user.addAliases([label: id]) } @@ -727,6 +728,7 @@ extension OneSignalUserManagerImpl: OSUser { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "addAliases") else { return } + if OSInputGuard.hasMissingEntries(aliases, "addAliases", allowEmptyValue: false) { return } user.addAliases(aliases) } @@ -734,6 +736,7 @@ extension OneSignalUserManagerImpl: OSUser { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "removeAlias") else { return } + if OSInputGuard.isMissing(label, "removeAlias: label") { return } user.removeAliases([label]) } @@ -741,6 +744,7 @@ extension OneSignalUserManagerImpl: OSUser { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "removeAliases") else { return } + if OSInputGuard.isMissingAny(labels, "removeAliases: label") { return } user.removeAliases(labels) } @@ -748,6 +752,7 @@ extension OneSignalUserManagerImpl: OSUser { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "addTag") else { return } + if OSInputGuard.isMissing(key, "addTag: key") { return } user.addTags([key: value]) } @@ -755,6 +760,7 @@ extension OneSignalUserManagerImpl: OSUser { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "addTags") else { return } + if OSInputGuard.hasMissingEntries(tags, "addTags", allowEmptyValue: true) { return } user.addTags(tags) } @@ -762,6 +768,7 @@ extension OneSignalUserManagerImpl: OSUser { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "removeTag") else { return } + if OSInputGuard.isMissing(tag, "removeTag: key") { return } user.removeTags([tag]) } @@ -769,6 +776,7 @@ extension OneSignalUserManagerImpl: OSUser { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "removeTags") else { return } + if OSInputGuard.isMissingAny(tags, "removeTags: key") { return } user.removeTags(tags) } @@ -783,6 +791,7 @@ extension OneSignalUserManagerImpl: OSUser { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "addEmail") else { return } + if OSInputGuard.isMissing(email, "addEmail: email") { return } // Check if is valid email? // Check if this email already exists on this User? createUserIfNil() @@ -806,6 +815,7 @@ extension OneSignalUserManagerImpl: OSUser { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "removeEmail") else { return } + if OSInputGuard.isMissing(email, "removeEmail: email") { return } // Check if is valid email? createUserIfNil() self.subscriptionModelStore.remove(email) @@ -815,6 +825,7 @@ extension OneSignalUserManagerImpl: OSUser { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "addSmsNumber") else { return } + if OSInputGuard.isMissing(number, "addSms: smsNumber") { return } // Check if is valid SMS? // Check if this SMS already exists on this User? createUserIfNil() @@ -838,6 +849,7 @@ extension OneSignalUserManagerImpl: OSUser { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "removeSmsNumber") else { return } + if OSInputGuard.isMissing(number, "removeSms: smsNumber") { return } // Check if is valid SMS? createUserIfNil() self.subscriptionModelStore.remove(number) @@ -862,6 +874,7 @@ extension OneSignalUserManagerImpl: OSUser { guard !OneSignalConfig.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "trackEvent") else { return } + if OSInputGuard.isMissing(name, "trackEvent: name") { return } let processedProperties = properties ?? [:] diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift new file mode 100644 index 0000000000..3c51aa9d8f --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift @@ -0,0 +1,78 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import OneSignalCore +import OneSignalCoreMocks +import OneSignalUserMocks +import XCTest +@testable import OneSignalOSCore +@_spi(OneSignalInternal) @testable import OneSignalUser + +final class InputGuardUserTests: XCTestCase { + func testBlankIdentityStringsAreNotStored() throws { + OneSignalCoreImpl.setSharedClient(MockOneSignalClient()) + let previousAppId = OneSignalIdentifiers.currentAppId + OneSignalIdentifiers.currentAppId = "b2f7f966-d8cc-11e4-bed1-df8f05be55ba" + defer { OneSignalIdentifiers.currentAppId = previousAppId } + + let manager = OneSignalUserManagerImpl.sharedInstance + + manager.addAlias(label: "", id: "alias-id") + manager.addAlias(label: "kept-label", id: "") + manager.addAliases(["blank-label": "x", "": "y"]) + XCTAssertNil(manager.user.identityModel.aliases[""]) + XCTAssertNil(manager.user.identityModel.aliases["kept-label"]) + XCTAssertNil(manager.user.identityModel.aliases["blank-label"]) + + manager.addAlias(label: " ", id: "space-id") + XCTAssertEqual(manager.user.identityModel.aliases[" "], "space-id") + manager.removeAlias(" ") + XCTAssertNil(manager.user.identityModel.aliases[" "]) + + manager.addTags(["": "nope", "blank-batch": "nope"]) + manager.addTag(key: "empty-value", value: "") + XCTAssertNil(manager.getTags()[""]) + XCTAssertNil(manager.getTags()["blank-batch"]) + XCTAssertEqual(manager.getTags()["empty-value"], "") + manager.removeTag("empty-value") + XCTAssertNil(manager.getTags()["empty-value"]) + + manager.addEmail("") + manager.removeEmail("") + XCTAssertNil(manager.subscriptionModelStore.getModel(key: "")) + + manager.addSms("") + manager.removeSms("") + XCTAssertNil(manager.subscriptionModelStore.getModel(key: "")) + + manager.removeAlias("") + manager.removeAliases([""]) + manager.removeTag("") + manager.removeTags([""]) + manager.trackEvent(name: "", properties: nil) + } +} diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index d8d58d094c..6e55af5233 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -243,6 +243,10 @@ + (void)logout { Note: wrappers may call this method with a null appId. */ + (void)initialize:(nonnull NSString*)newAppId withLaunchOptions:(nullable NSDictionary*)launchOptions { + // "" is rejected. nil still means the app id has not been provided. + if (newAppId != nil && [OSInputGuard isMissing:newAppId api:@"initialize: appId"]) { + return; + } [OSRemoteLoggingController configureFromCacheForAppId:newAppId ?: OneSignalIdentifiers.storedAppId]; [self setAppId:newAppId]; [self setLaunchOptions:launchOptions]; @@ -258,7 +262,12 @@ + (void)initialize:(nonnull NSString*)newAppId withLaunchOptions:(nullable NSDic + (void)setAppId:(nullable NSString*)newAppId { [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"setAppId called with appId: %@!", newAppId]]; - if (!newAppId || newAppId.length == 0) { + // "" is rejected. nil still falls back to the cached app id. + if (newAppId != nil && [OSInputGuard isMissing:newAppId api:@"initialize: appId"]) { + return; + } + + if (!newAppId) { NSString* cachedAppId = OneSignalIdentifiers.storedAppId; if (cachedAppId) { [OneSignalLog onesignalLog:ONE_S_LL_INFO message:[NSString stringWithFormat:@"Initializing OneSignal with cached appId: '%@'.", cachedAppId]]; From a2b5d520c6ec84befe6fa208901f0bb19bc96bb2 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Mon, 5 Oct 2026 12:55:35 -0500 Subject: [PATCH 2/5] fix: [SDK-5426] keep a nil trigger value off the dictionary The guard result was discarded, so a nil value still built the trigger dictionary. Tests now reset the shared user and cover the blank alias, tag, event, and trigger paths. Co-authored-by: Cursor --- .../OneSignalInAppMessages.m | 4 +- .../IAMIntegrationTests.swift | 17 +++++ .../IAMRequestTests.m | 20 ++++++ .../OSInputGuardTests.swift | 2 + .../InputGuardUserTests.swift | 64 +++++++++++++++---- 5 files changed, 90 insertions(+), 17 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.m index 1e4da8eaf7..223a173f75 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.m @@ -80,10 +80,8 @@ + (void)addTrigger:(NSString * _Nonnull)key withValue:(NSString * _Nonnull)value if ([OSInputGuard isMissing:key api:@"addTrigger: key"]) return; - if (!value) { - [OSInputGuard isMissing:nil api:@"addTrigger: value"]; + if (!value && [OSInputGuard isMissing:nil api:@"addTrigger: value"]) return; - } [OSMessagingController.sharedInstance addTriggers:@{key : value}]; } diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift index ff466300f9..a63499cf1b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift @@ -151,4 +151,21 @@ final class IAMIntegrationTests: XCTestCase { XCTAssertFalse(OSMessagingController.sharedInstance().isInAppMessageShowing) XCTAssertEqual(OSMessagingController.sharedInstance().messageDisplayQueue.count, 0) } + + func testBlankTriggerKeysLeaveExistingTriggers() { + let controller = OSMessagingController.sharedInstance() + OneSignalInAppMessages.addTrigger("guard-kept", withValue: "yes") + OneSignalInAppMessages.addTrigger("", withValue: "no") + OneSignalInAppMessages.addTriggers(["": "y", "blank-batch": "x"]) + OneSignalInAppMessages.removeTrigger("") + OneSignalInAppMessages.removeTriggers(["guard-kept", ""]) + + let triggers = controller.triggerController.getTriggers() + XCTAssertEqual(triggers["guard-kept"] as? String, "yes") + XCTAssertNil(triggers[""]) + XCTAssertNil(triggers["blank-batch"]) + + OneSignalInAppMessages.removeTrigger("guard-kept") + controller.earlySessionTriggers.remove("guard-kept") + } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMRequestTests.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMRequestTests.m index 6d5eb45098..36e910d56b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMRequestTests.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMRequestTests.m @@ -28,6 +28,14 @@ of this software and associated documentation files (the "Software"), to deal #import #import "OSInAppMessageBridgeEvent.h" #import "OSInAppMessagingRequests.h" +#import "OneSignalInAppMessages.h" +#import "OSMessagingController.h" +#import "OSTriggerController.h" + +@interface OSMessagingController (InputGuardTests) +@property (strong, nonatomic, nonnull) NSMutableSet *earlySessionTriggers; +@property (strong, nonatomic, nonnull) OSTriggerController *triggerController; +@end @interface IAMRequestTests : XCTestCase @@ -179,4 +187,16 @@ - (void)testInAppMessageClicked_withNilArguments { })); } +- (void)testNilTriggerValueIsNotStored { + [OneSignalInAppMessages addTrigger:@"nil-kept" withValue:@"yes"]; + [OneSignalInAppMessages addTrigger:@"nil-value" withValue:nil]; + + NSDictionary *triggers = [OSMessagingController.sharedInstance.triggerController getTriggers]; + XCTAssertEqualObjects(triggers[@"nil-kept"], @"yes"); + XCTAssertNil(triggers[@"nil-value"]); + + [OneSignalInAppMessages removeTrigger:@"nil-kept"]; + [OSMessagingController.sharedInstance.earlySessionTriggers removeObject:@"nil-kept"]; +} + @end diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSInputGuardTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSInputGuardTests.swift index cc730c634d..5134d5139d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSInputGuardTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSInputGuardTests.swift @@ -38,11 +38,13 @@ final class OSInputGuardTests: XCTestCase { func testIsMissingAnyRejectsABlankEntryAndANilCollection() { XCTAssertTrue(OSInputGuard.isMissingAny(nil as NSArray?, "removeTags: key")) + XCTAssertTrue(OSInputGuard.isMissingAny(nil as [String]?, "removeTags: key")) XCTAssertTrue(OSInputGuard.isMissingAny(["ok", ""], "removeTags: key")) XCTAssertFalse(OSInputGuard.isMissingAny(["ok", " "], "removeTags: key")) } func testHasMissingEntriesRejectsBlankKeysAndBlankAliasIds() { + XCTAssertTrue(OSInputGuard.hasMissingEntries(nil as [String: String]?, "addAliases", allowEmptyValue: false)) XCTAssertTrue(OSInputGuard.hasMissingEntries(["": "id"], "addAliases", allowEmptyValue: false)) XCTAssertTrue(OSInputGuard.hasMissingEntries(["label": ""], "addAliases", allowEmptyValue: false)) XCTAssertFalse(OSInputGuard.hasMissingEntries(["label": "id"], "addAliases", allowEmptyValue: false)) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift index 3c51aa9d8f..2ab3cf4e5c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift @@ -33,46 +33,82 @@ import XCTest @_spi(OneSignalInternal) @testable import OneSignalUser final class InputGuardUserTests: XCTestCase { - func testBlankIdentityStringsAreNotStored() throws { - OneSignalCoreImpl.setSharedClient(MockOneSignalClient()) - let previousAppId = OneSignalIdentifiers.currentAppId + private var previousAppId: String? + + override func setUpWithError() throws { + OneSignalCoreMocks.clearUserDefaults() + OneSignalUserMocks.reset() + previousAppId = OneSignalIdentifiers.currentAppId OneSignalIdentifiers.currentAppId = "b2f7f966-d8cc-11e4-bed1-df8f05be55ba" - defer { OneSignalIdentifiers.currentAppId = previousAppId } + OneSignalCoreImpl.setSharedClient(MockOneSignalClient()) + OSOperationRepo.sharedInstance.paused = true + } - let manager = OneSignalUserManagerImpl.sharedInstance + override func tearDownWithError() throws { + OneSignalIdentifiers.currentAppId = previousAppId + OneSignalCoreMocks.clearUserDefaults() + OneSignalUserMocks.reset() + } + func testBlankAliasLabelIsNotStoredAndEmptyRemoveLeavesIt() { + let manager = OneSignalUserManagerImpl.sharedInstance + manager.user.addAliases(["": "legacy-id"]) manager.addAlias(label: "", id: "alias-id") + manager.addAlias(label: "kept-label", id: "real-id") manager.addAlias(label: "kept-label", id: "") manager.addAliases(["blank-label": "x", "": "y"]) - XCTAssertNil(manager.user.identityModel.aliases[""]) - XCTAssertNil(manager.user.identityModel.aliases["kept-label"]) + + XCTAssertEqual(manager.user.identityModel.aliases[""], "legacy-id") + XCTAssertEqual(manager.user.identityModel.aliases["kept-label"], "real-id") XCTAssertNil(manager.user.identityModel.aliases["blank-label"]) + manager.removeAlias("") + manager.removeAliases([""]) + XCTAssertEqual(manager.user.identityModel.aliases[""], "legacy-id") + manager.addAlias(label: " ", id: "space-id") XCTAssertEqual(manager.user.identityModel.aliases[" "], "space-id") manager.removeAlias(" ") XCTAssertNil(manager.user.identityModel.aliases[" "]) + } + func testBlankTagKeyRejectsTheBatchAndEmptyRemoveLeavesIt() { + let manager = OneSignalUserManagerImpl.sharedInstance + manager.user.addTags(["": "legacy"]) manager.addTags(["": "nope", "blank-batch": "nope"]) manager.addTag(key: "empty-value", value: "") - XCTAssertNil(manager.getTags()[""]) + + XCTAssertEqual(manager.getTags()[""], "legacy") XCTAssertNil(manager.getTags()["blank-batch"]) XCTAssertEqual(manager.getTags()["empty-value"], "") + + manager.removeTag("") + manager.removeTags([""]) + XCTAssertEqual(manager.getTags()[""], "legacy") manager.removeTag("empty-value") XCTAssertNil(manager.getTags()["empty-value"]) + } + func testEmptyEmailAndSmsAreNotStored() { + let manager = OneSignalUserManagerImpl.sharedInstance manager.addEmail("") manager.removeEmail("") - XCTAssertNil(manager.subscriptionModelStore.getModel(key: "")) - manager.addSms("") manager.removeSms("") XCTAssertNil(manager.subscriptionModelStore.getModel(key: "")) + } + + func testBlankEventNameIsNotEnqueued() { + let manager = OneSignalUserManagerImpl.sharedInstance + let repo = OSOperationRepo.sharedInstance + manager.trackEvent(name: "kept-event", properties: nil) + repo.flushAndWait() + let before = repo.deltaQueue.filter { $0.name == OS_CUSTOM_EVENT_DELTA }.count - manager.removeAlias("") - manager.removeAliases([""]) - manager.removeTag("") - manager.removeTags([""]) manager.trackEvent(name: "", properties: nil) + repo.flushAndWait() + let after = repo.deltaQueue.filter { $0.name == OS_CUSTOM_EVENT_DELTA }.count + XCTAssertEqual(after, before) + XCTAssertGreaterThan(before, 0) } } From 91bf646f14082c9729070a23215b735016a080bf Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Tue, 6 Oct 2026 08:55:47 -0500 Subject: [PATCH 3/5] test: [SDK-5426] catch a missing email and SMS guard Assert after each add, and seed a blank subscription before the removes. Co-authored-by: Cursor --- .../InputGuardUserTests.swift | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift index 2ab3cf4e5c..98a0e61556 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift @@ -92,10 +92,26 @@ final class InputGuardUserTests: XCTestCase { func testEmptyEmailAndSmsAreNotStored() { let manager = OneSignalUserManagerImpl.sharedInstance manager.addEmail("") - manager.removeEmail("") + XCTAssertNil(manager.subscriptionModelStore.getModel(key: "")) manager.addSms("") - manager.removeSms("") XCTAssertNil(manager.subscriptionModelStore.getModel(key: "")) + + manager.subscriptionModelStore.add( + id: "", + model: OSSubscriptionModel( + type: .email, + address: "", + subscriptionId: nil, + reachable: true, + isDisabled: false, + changeNotifier: OSEventProducer() + ), + hydrating: false + ) + manager.removeEmail("") + XCTAssertNotNil(manager.subscriptionModelStore.getModel(key: "")) + manager.removeSms("") + XCTAssertNotNil(manager.subscriptionModelStore.getModel(key: "")) } func testBlankEventNameIsNotEnqueued() { From 7bb6d3dc6c7a436ee6f0675f7e57b370e5177161 Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Tue, 6 Oct 2026 11:28:57 -0500 Subject: [PATCH 4/5] fix: [SDK-5426] reject identity strings that contain a null byte A NUL cannot be stored in a text column. isMissing now rejects it, including login. Empty tag and trigger values still may contain one, matching Android. Co-authored-by: Cursor --- .../OneSignalOSCore/Source/OSInputGuard.swift | 7 ++++++- .../OSInputGuardTests.swift | 5 +++++ .../Source/OneSignalUserManagerImpl.swift | 5 +---- .../InputGuardUserTests.swift | 17 +++++++++++++++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSInputGuard.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSInputGuard.swift index 50f0bd6284..726aceb2a2 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSInputGuard.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSInputGuard.swift @@ -32,6 +32,11 @@ import OneSignalCore public final class OSInputGuard: NSObject { @objc(isMissing:api:) public static func isMissing(_ value: String?, _ api: String) -> Bool { + // A NUL cannot be stored in a text column, so it is never a usable value. + if let value, value.contains("\u{0000}") { + OneSignalLog.onesignalLog(.LL_ERROR, message: "[OneSignal] \(api) contains a null byte") + return true + } if let value, !value.isEmpty { return false } @@ -50,7 +55,7 @@ public final class OSInputGuard: NSObject { return false } - /// `allowEmptyValue` keeps "" and still rejects a null value. + /// `allowEmptyValue` keeps "" and a value containing a null byte. A null value is still rejected. @objc(hasMissingEntries:api:allowEmptyValue:) public static func hasMissingEntries( _ values: NSDictionary?, diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSInputGuardTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSInputGuardTests.swift index 5134d5139d..2f610d093e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSInputGuardTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSInputGuardTests.swift @@ -34,6 +34,8 @@ final class OSInputGuardTests: XCTestCase { XCTAssertTrue(OSInputGuard.isMissing("", "login: externalId")) XCTAssertFalse(OSInputGuard.isMissing(" ", "login: externalId")) XCTAssertFalse(OSInputGuard.isMissing("user", "login: externalId")) + XCTAssertTrue(OSInputGuard.isMissing("\u{0000}: 1", "login: externalId")) + XCTAssertTrue(OSInputGuard.isMissing("abc\u{0000}", "addAlias: id")) } func testIsMissingAnyRejectsABlankEntryAndANilCollection() { @@ -49,6 +51,7 @@ final class OSInputGuardTests: XCTestCase { XCTAssertTrue(OSInputGuard.hasMissingEntries(["label": ""], "addAliases", allowEmptyValue: false)) XCTAssertFalse(OSInputGuard.hasMissingEntries(["label": "id"], "addAliases", allowEmptyValue: false)) XCTAssertFalse(OSInputGuard.hasMissingEntries(["label": " "], "addAliases", allowEmptyValue: false)) + XCTAssertTrue(OSInputGuard.hasMissingEntries(["external_id": "\u{0000}: 1"], "addAliases", allowEmptyValue: false)) } func testHasMissingEntriesAllowsAnEmptyTagValueAndRejectsNull() { @@ -57,5 +60,7 @@ final class OSInputGuardTests: XCTestCase { let tags = NSMutableDictionary() tags["key"] = NSNull() XCTAssertTrue(OSInputGuard.hasMissingEntries(tags, "addTags", allowEmptyValue: true)) + XCTAssertFalse(OSInputGuard.hasMissingEntries(["key": "a\u{0000}b"], "addTags", allowEmptyValue: true)) + XCTAssertTrue(OSInputGuard.hasMissingEntries(["a\u{0000}": "value"], "addTags", allowEmptyValue: true)) } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index e3c34af6bb..e99553aea3 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -333,10 +333,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { return } start() - guard externalId != "" else { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OneSignal.User login called with empty externalId. This is not allowed.") - return - } + if OSInputGuard.isMissing(externalId, "login: externalId") { return } OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OneSignal.User login called with externalId: \(externalId)") // Logging into an identified user from an anonymous user diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift index 98a0e61556..6f11246aff 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/InputGuardUserTests.swift @@ -114,6 +114,23 @@ final class InputGuardUserTests: XCTestCase { XCTAssertNotNil(manager.subscriptionModelStore.getModel(key: "")) } + func testNullByteIsNotStoredExceptAsAnEmptyTagValue() { + let manager = OneSignalUserManagerImpl.sharedInstance + manager.login(externalId: "\u{0000}: 1", token: nil) + XCTAssertNil(manager.externalId) + + manager.addAlias(label: "nul-id", id: "\u{0000}: 1") + manager.addAlias(label: "\u{0000}", id: "1") + XCTAssertNil(manager.user.identityModel.aliases["nul-id"]) + XCTAssertNil(manager.user.identityModel.aliases["\u{0000}"]) + + manager.addTag(key: "nul-value", value: "a\u{0000}b") + XCTAssertEqual(manager.getTags()["nul-value"], "a\u{0000}b") + manager.addTags(["\u{0000}": "nope", "sibling": "nope"]) + XCTAssertNil(manager.getTags()["sibling"]) + manager.removeTag("nul-value") + } + func testBlankEventNameIsNotEnqueued() { let manager = OneSignalUserManagerImpl.sharedInstance let repo = OSOperationRepo.sharedInstance From fd4f56a2259c520ddd695b653390bf1a0d94184a Mon Sep 17 00:00:00 2001 From: AR Abdul Azeez Date: Tue, 6 Oct 2026 13:03:32 -0500 Subject: [PATCH 5/5] style: [SDK-5426] drop the redundant [OneSignal] log prefix Co-authored-by: Cursor --- .../OneSignalSDK/OneSignalOSCore/Source/OSInputGuard.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSInputGuard.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSInputGuard.swift index 726aceb2a2..20a2efa34c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSInputGuard.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSInputGuard.swift @@ -34,13 +34,13 @@ public final class OSInputGuard: NSObject { public static func isMissing(_ value: String?, _ api: String) -> Bool { // A NUL cannot be stored in a text column, so it is never a usable value. if let value, value.contains("\u{0000}") { - OneSignalLog.onesignalLog(.LL_ERROR, message: "[OneSignal] \(api) contains a null byte") + OneSignalLog.onesignalLog(.LL_ERROR, message: "\(api) contains a null byte") return true } if let value, !value.isEmpty { return false } - OneSignalLog.onesignalLog(.LL_ERROR, message: "[OneSignal] \(api) is required") + OneSignalLog.onesignalLog(.LL_ERROR, message: "\(api) is required") return true }