fix: replace deprecated NSKeyedArchiver/NSKeyedUnarchiver APIs#1680
fix: replace deprecated NSKeyedArchiver/NSKeyedUnarchiver APIs#1680garvitkaushik-123 wants to merge 1 commit into
Conversation
…odern equivalents The deprecated `+[NSKeyedUnarchiver unarchiveObjectWithData:]` and `+[NSKeyedArchiver archivedDataWithRootObject:]` methods are flagged as unsafe by security scanners (CWE-676). Replace them with instance-based `NSKeyedUnarchiver` and `archivedDataWithRootObject:requiringSecureCoding:error:` which are available since iOS 11.0 (the SDK minimum deployment target). Resolves OneSignal#919
|
@claude review |
| - (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue:(id _Nullable)value { | ||
| if ([self keyExists:key]) | ||
| return [NSKeyedUnarchiver unarchiveObjectWithData:[self.userDefaults objectForKey:key]]; | ||
|
|
||
| if ([self keyExists:key]) { | ||
| NSData *data = [self.userDefaults objectForKey:key]; | ||
| NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:data error:nil]; | ||
| unarchiver.requiresSecureCoding = NO; | ||
| id result = [unarchiver decodeTopLevelObjectAndReturnError:nil]; | ||
| [unarchiver finishDecoding]; | ||
| return result; | ||
| } | ||
|
|
There was a problem hiding this comment.
🟡 Minor behavior change to flag: the old +[NSKeyedUnarchiver unarchiveObjectWithData:] threw NSException on decode failures, which migrateIAMRedisplayCache in OSInAppMessageMigrationController.m:50-68 catches to trigger corrupt-cache recovery. The new initForReadingFromData:error: + decodeTopLevelObjectAndReturnError: pair is documented to convert exceptions to NSError and return nil for some failure modes, so the recovery @catch may no longer fire for genuinely-corrupt archive bytes. Impact is bounded (only devices upgrading from SDK < 3.2.03, no crash — corrupt entry just stays stale until the sibling migrateToOSInAppMessageInternal clears it), but worth a defensive if (!result && data) { @throw ... } if you want to preserve the original migration contract.
Extended reasoning...
What changed
The PR replaces +[NSKeyedUnarchiver unarchiveObjectWithData:] with initForReadingFromData:error: + decodeTopLevelObjectAndReturnError:. Beyond the deprecation-warning fix, this changes the failure contract: the old API raised NSException on any decode failure (invalid archive bytes, missing class, wrong-type input), while the new API pair is explicitly designed to convert exceptions into NSError return values.
Where this matters
In OSInAppMessageMigrationController.m:50-68, migrateIAMRedisplayCache wraps getSavedCodeableDataForKey: in @try / @catch (NSException *) specifically to detect legacy corrupt cache data. The comment on line 41-43 states the intent: "Devices could potentially have bad data in the OS_IAM_REDISPLAY_DICTIONARY that was saved as a dictionary and not CodeableData. Try to detect if that is the case…". When the @catch fires, the code re-reads the value as an NSDictionary and re-saves it as archived data.
Step-by-step trace (corrupt-NSData subcase)
- Device has SDK version < 30203 in
NSUserDefaultsand a corruptNSDatablob stored underOS_IAM_REDISPLAY_DICTIONARY(e.g., partial write, unknown archive class). getSavedCodeableDataForKey:runs —[[NSKeyedUnarchiver alloc] initForReadingFromData:data error:nil]returns nil (per Apple docs for malformed archive headers).unarchiver.requiresSecureCoding = NOis a no-op (message to nil).[unarchiver decodeTopLevelObjectAndReturnError:nil]returns nil (message to nil).getSavedCodeableDataForKey:returnsnil— note it returnsresult(nil), notvalue(the default).- Back in
migrateIAMRedisplayCache:[[NSMutableDictionary alloc] initWithDictionary:nil]on modern Foundation returns an empty mutable dict without throwing. @catchnever fires. Recovery path is skipped.
Addressing the refutations
The strongest refutation argues (a) decodingFailurePolicy defaults to RaiseException and (b) wrong-type inputs (NSDictionary passed where NSData is expected) still raise NSInvalidArgumentException via unrecognized selector, so the @catch still fires for the specific "saved-as-dictionary" case the migration was built for. These points have merit — decodeTopLevelObjectAndReturnError: is documented to catch internal exceptions and convert to NSError, and messaging -bytes/-length on an NSDictionary really would throw. So for the primary target case named in the migration comment (NSDictionary stored via saveDictionaryForKey: earlier), the @catch likely still triggers.
However, the failure surface is narrower than before. Cases that previously all threw and now silently return nil include: (a) NSData with corrupt archive bytes, (b) archive referencing an unmapped class (relevant because migrateIAMRedisplayCache runs before migrateToOSInAppMessageInternal registers the OSInAppMessage → OSInAppMessageInternal class mapping on line 77), and (c) any decode failure that Apple has converted to error-return over the years. That is a real, documented API contract change on a code path whose whole reason for existing is to catch decode failures.
Severity rationale
Marking this a nit rather than blocking. Reasons: the migration is version-gated to sdkVersion < 30203 (SDK 3.02.03, ~2019); the affected user population — devices upgrading from that old SDK with genuinely corrupt cache — is small; the failure mode is silent (no crash, callers see empty state); and migrateToOSInAppMessageInternal runs immediately after and clears the key if the result is empty, so the stale data eventually gets wiped even if not migrated to the new format. The behavior change is worth flagging so the author can decide whether to preserve the original contract, but is not a merge blocker.
Suggested fix
If preserving the original migration contract is desired, capture the error and rethrow, e.g.:
- (id _Nullable)getSavedCodeableDataForKey:(NSString * _Nonnull)key defaultValue:(id _Nullable)value {
if ([self keyExists:key]) {
NSData *data = [self.userDefaults objectForKey:key];
NSError *error = nil;
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:data error:&error];
if (!unarchiver) {
@throw [NSException exceptionWithName:NSInvalidUnarchiveOperationException reason:error.localizedDescription userInfo:nil];
}
unarchiver.requiresSecureCoding = NO;
id result = [unarchiver decodeTopLevelObjectAndReturnError:&error];
[unarchiver finishDecoding];
if (!result && error) {
@throw [NSException exceptionWithName:NSInvalidUnarchiveOperationException reason:error.localizedDescription userInfo:nil];
}
return result;
}
return value;
}Alternatively, restructure migrateIAMRedisplayCache to trigger the recovery path when getSavedCodeableDataForKey: returns nil while the raw NSUserDefaults value is non-nil.
Summary
+[NSKeyedUnarchiver unarchiveObjectWithData:]with instance-basedNSKeyedUnarchiverusinginitForReadingFromData:error:anddecodeTopLevelObjectAndReturnError:+[NSKeyedArchiver archivedDataWithRootObject:]witharchivedDataWithRootObject:requiringSecureCoding:NO error:The deprecated methods are flagged as unsafe by security scanners (CWE-676). Setting
requiresSecureCoding = NOmaintains backward compatibility with existing model classes that implementNSCoding.Resolves #919
Test plan
NSKeyedArchiver/NSKeyedUnarchiverin build output