OWASP Top 10 (2021) & OWASP Mobile Top 10 (2024) Assessment — Re-check v4
v1.2.16 · 9 Jun 2026 UpdatedThis is a full re-assessment (Re-check v4) of the DailyCue iOS app. The application is a local-first, offline-capable reminder and routine tracker with no network communication — significantly reducing its attack surface.
Overall posture: Low. The app maintains zero network
attack surface (INTERNET permission not declared), uses iOS
Keychain for cryptographic secrets, applies NSFileProtectionCompleteUnlessOpen
to all database files, and uses GRDB parameterized queries throughout —
eliminating SQL injection. Since Re-check v3, the app has added routine
schedule reconciliation (database v5), a new notification queue
reconciler that respects iOS's 64-notification limit, and time-sensitive
interruption levels for critical notifications.
All 4 prior remediations remain fully closed.
F1 — CLOSED
Database encryption remains wired through Configuration.prepareDatabase.
F2 — CLOSED
Zero print() calls found — all logging uses Logger from OSLog.
F3 — CLOSED
UIBackgroundModes remains absent from Info.plist.
F4 — CLOSED
No regression of try?/empty catch patterns in new code;
all 7 new try? usages are for non-critical operations (Task.sleep, optional encoding).
New findings. Four new items were identified in the new
notification and reconciliation code. Two Low-severity findings: a type mismatch
in notification identifiers (Int vs Int64 — N1) and copy-paste-able .public
logging patterns (N4). Two Informational findings: a stale hardcoded appVersion in
the backup envelope (N2) and a tag-content heuristic for event classification (N3).
Existing findings F6 (resetPassphrase latent risk), F12 (updateField design smell),
and F13 (UserDefaults protection) remain open with unchanged risk profiles.
Configuration.prepareDatabaseprint() locations replaced with Logger calls using OSLogtry?/empty catch locations across 17 files REMEDIATED: All converted to do/try/catch with Logger error/warning loggingJSONDecoder does NOT default to .allowJSON5 (false by default)resetPassphrase() which could orphan the database (previously mislabeled as F7)SettingsRepository.updateField() uses dynamic SQL column name interpolation — currently safe but a SQL injection design smellcom.dailyq → com.dailycueplanner to match actual codebaseprint() statements introduced; all logging continues to use Logger from OSLogprint(), try?, or silent catch patterns in new codeConfiguration.prepareDatabase; new migration v5 is safe and additiveNotificationQueueReconciler actor replaces ad-hoc notification scheduling with set-based diffing, respecting iOS 64-notification capactor isolation, eliminating data races17 findings (0 critical, 0 high, 0 medium, 6 low, 7 informational, 4 remediated)
Status: FULLY REMEDIATED. DatabaseEncryption.swift provides a
Keychain-backed 32-byte passphrase, and DailyCueDatabase.swift now
applies it during database open via Configuration.prepareDatabase:
The build configuration (project.yml) points to the local
SQLCipher-backed GRDBCipher package. On fresh installs, the database is
created encrypted from the start. On upgrades from the plaintext era,
migratePlaintextDatabase() handles the migration:
"SQLite format 3\0")
Remaining (minor) concern: The migration involves an async
suspension point (try await restoreSnapshot at line 83) between
moving the plaintext file (line 77) and deleting the backup (line 82). If
the process is killed during this window, a plaintext backup file
(dailycue.db.plaintext-backup) could remain on disk. This is
mitigated by NSFileProtectionCompleteUnlessOpen on the directory
and the backup being left in the application support directory. Consider
wrapping the migration in an NSFileCoordinator scope or adding
a cleanup check on next launch.
Configuration.prepareDatabase#if canImport(SQLCipher) block is active
Status: FULLY REMEDIATED. All 6 print()
locations across the codebase have been replaced with structured
Logger calls from OSLog:
logger.warning("Backup HMAC mismatch...")logger.error("[loadChecklistItems] Batch fetch failed...")Logger(...).error("Settings persistence failed...")Logger(...).error("[QuickAdd] Failed to save...")Logger(...).info("[QuickAdd] Saved...")
Each file now imports OSLog and uses the appropriate log level:
.warning for security events (HMAC mismatch, file not found).error for unexpected operational failures.info for informational events
The messages are now captured in the unified log system, persist across
sessions, and can be inspected via log show or the Console app.
Each logger is categorized by subsystem (com.dailycueplanner) and
component (backup, home, settings,
quickadd, soundpreview).
print() calls replaced with Logger callsimport OSLog added to each affected file.error, .warning, .info
Status: FULLY REMEDIATED. The UIBackgroundModes: audio
declaration has been removed from Info.plist. This capability was previously
declared without functional justification — the app has no feature requiring
background audio playback.
SoundPreviewPlayer (SettingsView.swift) uses
AVAudioPlayer to preview notification sounds interactively
in the settings UI — this works correctly without the background audio
entitlement. Notification sounds delivered via UNNotificationSound
are handled by the system notification service and do not require it either.
The removal eliminates:
UIBackgroundModes key removed entirely from Info.plistAVAudioPlayer in the foreground
The original review identified 5 locations. The re-assessment found
~55 locations across 17 files using try?
or empty catch blocks that silently discard errors. Key
categories:
Category 1: AsyncStream catch with silent finish (9 locations)
catch { continuation.finish() }catch { continuation.finish() }Category 2: try? discarding errors at call sites (20+ locations)
let settings = try? await deps.settingsRepository.fetchSettings()
— settings fetch failure silently defaults_ = try? await deps.notificationManager.requestAuthorization()
— authorization failure silently ignored
While many of these are non-security operations (UI updates, cleanup), the
pervasive pattern makes debugging difficult and can mask subtle data
integrity issues. The try? pattern for settings fetch (F111)
and notification authorization (F125) are particularly concerning because
they affect core app functionality.
os_log(.error, ...) before each silent recovery pathdo/try/catch with explicit user-facing error handlingAsyncStream catch blocks, log the error before
calling continuation.finish()try?
☑ Status: FULLY REMEDIATED. All ~55 try?/empty
catch locations across 17 files have been converted to
do/try/catch with structured Logger error/warning
logging. The remediation covers:
catch
blocks now log via logger.error("Stream failed: ...") before
continuation.finish()do/catch with logger.errordo/catch with logger.error; 3
Task.sleep sites left as try? (cancellation expected)try? sites
converted to do/catch with logger.warning/logger.errortry? sites
converted with logger.error for notification scheduling failurestry? sites
converted with Logger for settings fetch + notification schedulingtry? sites
converted with logger.error for checklist/event cleanuptry? sites
converted with logger.warning for settings fetchdo/catch with logger.error; isPlaintextSQLiteDatabase
now logs on read failuredo/catch with Logger warning/errorimport OSLog added to all affected filesLogger instances use subsystem: "com.dailyq" with
appropriate category stringsdo/catch (not try?)logger.warninglogger.error
Correction from previous review: The previous review
incorrectly stated that JSONDecoder() defaults to
.allowJSON5. In iOS 15.0+ (the app's minimum target),
allowsJSON5 defaults to false, so the decoder
is already strict about JSON syntax. This finding has been corrected.
BackupManager.importFrom() uses JSONDecoder()
to decode user-provided backup files. While the checksum and HMAC provide
integrity verification after JSON parsing, the decoder itself has two
remaining concerns:
JSONDecoder has no built-in max depth limit.BackupEnvelopeCodable and BackupPayloadCodable
types use synthesized Codable initializers, which ignore
extra fields in the JSON. While not a vulnerability, this means a malformed
but valid-JSON backup could be partially decoded without error.Task.timeout = 5.0 using a cooperative timeout)JSONSerializationJSONSerialization.isValidJSONObject() as a pre-filter
DatabaseEncryption.swift:21–25 defines
resetPassphrase() which deletes the existing Keychain-stored
passphrase and generates a new one:
If this method were called after the database has already been opened with the old passphrase, the database file would become permanently inaccessible:
PRAGMA rekey call
Mitigating factor: resetPassphrase() is
not called anywhere in the current codebase. However, it
is a public API on DatabaseEncryption and could be called
inadvertently by future code.
resetPassphrase() entirely, or implement it
properly with a matching SQLCipher PRAGMA rekey operation
that re-encrypts the database with the new passphrase. If kept for future
use, add documentation warning that re-keying must follow.
DatabaseEncryption.swift and BackupKeyStore.swift correctly use the iOS Keychain for cryptographic material:
SecRandomCopyBytes, stored in the Keychain
with kSecAttrAccessibleWhenUnlockedThisDeviceOnly —
meaning the passphrase cannot be extracted from backups or when the
device is lockedSymmetricKey
stored in the Keychain as a generic key with
kSecAttrAccessibleWhenUnlockedThisDeviceOnly protection,
preventing backup replay from other devicesUUID or Date) and encodes as hex
for usabilityOSStatus in the
DatabaseError / BackupError typescachedKey in
BackupKeyStore) reduces Keychain lookups while still loading from the
secure store on first accessThis is a model implementation of iOS credential storage, now fully wired to the database encryption layer (see Finding 1 — remediated).
The iOS app contains zero HTTP, socket, or other network
communication code. No INTERNET permission is declared in the
Info.plist. No URLSession, WKWebView, or
NWConnection usage exists anywhere in the codebase. There are
no third-party analytics, crash reporting SDKs, or network-dependent
dependencies (GRDB is a local SQLite wrapper). This eliminates the largest
class of mobile app vulnerabilities entirely and mirrors the Android app's
strong posture.
All database queries across the codebase use GRDB's query builder or
parameterized SQL (positional ? placeholders). GRDB binds
these as SQLite prepared statement parameters, making SQL injection
impossible even if user-supplied data flows into queries. Examples:
All repository code uses parameterized queries exclusively. No dynamic SQL construction via string interpolation exists anywhere in the codebase — with one exception.
Design smell — SettingsRepository.updateField():
SettingsRepository.swift:107–119 uses string interpolation for
the column name:
Why this is currently safe: The updateField
method is private and all 18 callers pass hardcoded string
literals for column names (e.g., "reminderDurationMinutes").
There is no path where user input reaches the field parameter.
The value portion is properly parameterized via ?
bindings.
Why it's a design smell: If a future developer adds a new
caller that passes a dynamically-constructed column name (e.g., from user
input or a config key), this would become a SQL injection vector. Column
names cannot be parameterized in SQLite — they must be either interpolated
or whitelisted. The GRDB query builder (Column("name")) would
prevent this entirely.
updateField() pattern with a GRDB query
builder approach or a whitelist of permitted column namesfield must only receive hardcoded literalsarguments: parameter with positional
bindings rather than string interpolation for valuesBackupManager.swift implements a two-layer backup integrity scheme:
BackupError.integrityError)CryptoKit.HMAC. On import, the HMAC
is verified. A mismatch triggers a warning (not a hard error — to support
cross-device migration)startAccessingSecurityScopedResource /
stopAccessingSecurityScopedResource with
defer for file importer URLsData.WritingOptions.atomic to prevent partial file writesNotificationRescheduler cancels and re-creates all
pending notifications to match the restored schedule
The BackupKeyStore implementation correctly stores the
HMAC key in the Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
making the key device-specific and unrecoverable from backups.
SmsComposerPresenter.swift uses Apple's
MFMessageComposeViewController API for sending routine
completion SMS messages. This is the correct and only supported approach
on iOS:
SEND_SMS, iOS has no such permission.
MFMessageComposeViewController presents the system Messages
composer and the user must tap Send manuallycanSendText()
is checked before presenting, gracefully handling devices without SMS
capability (iPad, iPod touch, simulator)CNContactPickerViewController
with a predicate for contacts with phone numbers. The
NSContactsUsageDescription in Info.plist explains the
purposecontactsJson column in routines), which is now protected
by both NSFileProtectionCompleteUnlessOpen at rest and
SQLCipher database encryptionSMS messages are inherently unencrypted (SMS protocol limitation). The message content is non-sensitive ("DailyCue notification: [name] just completed the [routine] routine! ✅"), and the feature requires explicit user opt-in (SMS must be enabled in settings, contacts must be added to routines).
SettingsRepository.swift:107–119 defines a private helper
updateField() that interpolates the column name directly into
the SQL string:
Why this is currently safe (mitigating factors):
private — only accessible within
SettingsRepository itself"reminderDurationMinutes", "themeName",
"ownerName")value portion is properly bound via positional
? parameterization — no user data reaches the column nameWhy it's a design smell:
internal or public) or a future caller
accepts a field name from user input, this becomes an exploitable
SQL injection vectorColumn("name")) handles column
references safely and prevents typos — switching to it would eliminate
this risk category entirelyupdateField() to use a column name whitelist
(e.g., a dictionary mapping operation types to column names) instead
of raw string interpolationupdateField callers
with direct GRDB property updates on SettingsRecord
(e.g., record.reminderDurationMinutes = minutes; try record.update(db))field
parameter must only accept hardcoded literals
WalkthroughRepository.swift and PredefinedRoutinesSeeder.swift
use UserDefaults.standard for persisting walkthrough progress and first-launch
seeding state:
has_seeded_predefined_routines boolean
in UserDefaults
This is an inconsistency in the app's data protection strategy. The database files receive
NSFileProtectionCompleteUnlessOpen (DailyCueDatabase.swift:114–117) and
SQLCipher encryption, while Keychain items use
kSecAttrAccessibleWhenUnlockedThisDeviceOnly. In contrast, UserDefaults plist
files are stored without an explicit file protection level and rely solely on the device's
keybag encryption.
Risk assessment:
NSFileProtectionCompleteUntilFirstUserAuthentication from the app's sandbox by
default, providing moderate at-rest protectionNSFileProtectionCompleteUntilFirstUserAuthentication
to the UserDefaults plist
NotificationManager.swift accepts Int parameters for
scheduleNotification(id:) (line 54) and scheduleSnoozedNotification(id:)
(line 77) notification identifiers, but event IDs throughout the codebase are typed as
Int64. The identifier string is built via string interpolation:
On 64-bit iOS, Int and Int64 are the same width, so the
truncation risk is theoretical. However, the type mismatch means the API surface
communicates the wrong contract — a caller passing an Int from a
truncated source (e.g., a 32-bit value cast to Int) would silently
produce a different identifier than expected, leading to duplicate notifications
or failed cancellation.
scheduleNotification(id:) and
scheduleSnoozedNotification(id:) parameter types from
Int to Int64 for consistency with the rest of the codebaseString) to prevent type confusionBackupManager.exportTo() (line 77) embeds a hardcoded version string in the backup envelope:
This string was "1.2.6" during the original review, was updated to
"1.2.13" during Re-check v3 work but has not been kept in sync with
the actual app version (currently 1.2.16). The backup integrity is independently
protected by SHA-256 checksum and HMAC signing, so this does not affect security.
However, it could confuse cross-version restore debugging and is a maintenance
hygiene concern.
Bundle.main.infoDictionary at runtime
(e.g., Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString"))
instead of hardcoding
ScheduleRepository.swift differentiates routine vs. appointment
events by checking whether the tags column contains the substring
"routine":
This heuristic appears in both deleteRoutinesByTitle() and
deleteFutureAppointmentsByTitle(). While tags are set to
"routine" during event creation for routine-generated events, the
Event model makes tags a user-editable string field. A user who
creates an appointment with the word "routine" in the tags (e.g., "daily routine
checkup") would have that event incorrectly classified during bulk operations.
generatedRoutineId column (added in database v5)
for routine identification instead of the tag heuristictags = "routine") rather than LIKE '%routine%'
NotificationRescheduler.swift (lines 75-76, 137) uses
privacy: .public for reconciliation context strings:
The context values themselves are simple identifiers ("launch",
"settings", "delete-event") and do not contain sensitive
data. However, the .public pattern could be copy-pasted into future
logging statements where the interpolated value is sensitive (e.g., user
names, event titles, phone numbers). Swift's OSLog by default redacts
dynamic values — .public must be explicitly opted into.
.public with .private (or omit the
privacy parameter entirely, which defaults to .private on
iOS 15+) for any future logging of user-supplied values.public is appropriate for these specific values.public must be justified
with a comment explaining why the logged value is non-sensitive| Category | Risk Level | Key Findings |
|---|---|---|
| A01 — Broken Access Control | Low | Positive controls: zero network (F8), no deep links, no exported activities/components, notification tap identifiers are internal ("dailycue-<eventId>"). No auth required (local-only app). |
| A02 — Cryptographic Failures | Low | F1 (database encryption — remediated, now SQLCipher-backed), F7 (Keychain usage — positive), F6 (resetPassphrase latent risk — Low). Database at rest protected by both SQLCipher and NSFileProtection. HMAC signing is correctly implemented. F13 (UserDefaults without explicit file protection — Low, non-sensitive data only). |
| A03 — Injection | Low | No SQL injection risk from parameterized queries (F9 — GRDB). Minor design
smell: SettingsRepository.updateField() uses string interpolation
for column names (F12 — Low, currently safe with private scope and hardcoded
callers). No WebView (no XSS). No dynamic string evaluation. |
| A04 — Insecure Design | Low | Clean MVVM + Repository + UseCase architecture with Swift actor isolation. Single source of truth for settings (GRDB). F12 (SQL column interpolation — Low), F14 (notification ID type mismatch — Low), F16 (tag heuristic — Info). |
| A05 — Security Misconfiguration | Low | F3 (UIBackgroundModes: audio — remediated, removed from Info.plist). F15 (stale appVersion in backup — Info). No exposed debug endpoints or backdoors. |
| A06 — Vulnerable & Outdated Components | Low | Single dependency: GRDB/SQLCipher (local package). Swift 6.0 compiler. Xcode 26.5. No transitive dependencies with known vulnerabilities. |
| A07 — Identification & Authentication Failures | N/A | No authentication required (local-only app with no user accounts). |
| A08 — Software & Data Integrity Failures | Low | F5 (JSONDecoder — nested depth risk, Low), F10 (SHA-256 + HMAC backup integrity — positive). Backup import has validation (size, version, checksum, HMAC). Atomic writes on export. |
| A09 — Security Logging & Monitoring Failures | Low | F2 (print() → os_log — remediated), F4 (try? → Logger — remediated), F17 (.public logging pattern — Low, pattern risk). |
| A10 — SSRF | N/A | No server-side requests. No URL fetching. Zero network code (F8). |
| Category | Risk Level | Notes |
|---|---|---|
| M1 — Improper Platform Usage | Low | F3 (UIBackgroundModes: audio — remediated, removed from Info.plist). Contact picker uses CNContactPickerViewController correctly. SMS uses MFMessageComposeViewController correctly (F11). Notification sound authorization requested on first launch. |
| M2 — Insecure Data Storage | Low | F1 remediated — SQLCipher encryption now active on database. All user data (owner name, phone numbers, routine/event details) encrypted at rest. NSFileProtectionCompleteUnlessOpen applied as defense in depth. Keychain secrets stored with accessibleWhenUnlockedThisDeviceOnly (F7 — positive). F13 (UserDefaults without explicit file protection — Low, non-sensitive data only). |
| M3 — Insecure Communication | Low | No network communication at all (F8 — positive). SMS messages are inherently unencrypted (by protocol design, not app flaw — F11). |
| M4 — Insecure Authentication | N/A | No authentication in a local-only app. |
| M5 — Insufficient Cryptography | Low | F1 remediated — database now encrypted via SQLCipher. HMAC-SHA256 for backups is correct (F7, F10). SecRandomCopyBytes for passphrase generation. Keychain accessibility settings are appropriate. F6 (resetPassphrase latent risk — Low, no rekey implementation). F13 (UserDefaults without explicit file protection — Low, non-sensitive data). |
| M6 — Insecure Authorization | N/A | Single-user local app with no authorization model needed. |
| M7 — Client Code Quality | Low | Clean MVVM + Repository + UseCase architecture. F4 (~55 try?/empty catch blocks — remediated). F12 (SQL column interpolation — Low). F14 (notification ID type mismatch — Low). F16 (tag heuristic — Info). Consistent patterns across the codebase (Swift 6.0, modern concurrency, actor isolation). |
| M8 — Code Tampering | Low | Code signing uses standard Xcode automatic signing. No code integrity checks (acceptable for local app). Backup HMAC provides tampering detection for backup files (F10). |
| M9 — Reverse Engineering | Low | Swift code compiles to native binaries with default debug symbol stripping for release builds. No explicit obfuscation (Swift has no ProGuard equivalent). Acceptable for a local-only app with no API secrets. |
| M10 — Extraneous Functionality | Low | F3 (UIBackgroundModes: audio — remediated, removed from Info.plist). F6 (resetPassphrase defined but unused — Low). F12 (updateField dynamic SQL — Low design smell). No debug backdoors, test endpoints, or developer-only functionality present in the codebase. |
| Vector | Present | Notes |
|---|---|---|
| Network (HTTP/WebSocket) | None | Zero network code. No INTERNET permission or URLSession usage. |
| WebView / JavaScript | None | No WKWebView or UIWebView usage anywhere in the app. |
| File I/O | Minimal | Database in Application Support directory (NSFileProtection + SQLCipher). Backup accessed via security-scoped URL (SAF-equivalent). No direct file path handling for user data. |
| Deep Links / URL Schemes | None | No declared URL schemes or universal links. No intent filters. |
| Notification Deep Links | Removed | Notification deep-link parsing removed; all notification taps now navigate to Home. Reduces attack surface from identifier parsing. |
| Backup / Data Export | SAF-gated | User selects file via UIDocumentPickerViewController. SHA-256 + HMAC verified on import. 10 MB size limit. Version validation. Atomic writes. |
| SMS (opt-in) | Low | MFMessageComposeViewController (user must tap Send). No SEND_SMS permission. Device capability check. Explicit user opt-in (settings + contacts). |
| Database Encryption | Remediated | SQLCipher encryption active. NSFileProtectionCompleteUnlessOpen as defense in depth. Plaintext migration path implemented. |
| Background Modes | None | UIBackgroundModes: audio removed from Info.plist. No background modes declared. SoundPreviewPlayer works in the foreground only. |
| UserDefaults | Low | Used for walkthrough state + seeding flags. No explicit file protection (inherits iOS default: CompleteUntilFirstUserAuthentication). Non-sensitive data only (F13). |
| Priority | Finding | Effort | Recommendation |
|---|---|---|---|
| DONE | Wire DatabaseEncryption passphrase into GRDB (F1) | Medium | ✅ Remediated. SQLCipher passphrase applied via Configuration.prepareDatabase. Plaintext migration path implemented. Consider adding cleanup check for partial migration artifacts. |
| DONE | Replace all print() with os_log (F2 — 6 locations) | Low | ✅ Remediated. All print() calls replaced with Logger from OSLog. Structured logging with proper levels. |
| DONE | Remove or justify UIBackgroundModes: audio (F3) | Low | ✅ Remediated. Key removed from Info.plist. |
| DONE | Add os_log to all silent catch/try? sites (F4 — ~55 locations in 17 files) | Medium | ✅ Remediated. All ~55 locations converted to do/try/catch with Logger from OSLog. Critical init paths use do/catch with error logging; operational paths use logger.warning/error. Imports OSLog in all affected files. Build verified. |
| P2 | Harden backup JSON parsing (F5) | Low | Add JSON nesting depth check or parsing timeout. Pre-validate with JSONSerialization. |
| P3 | Remove or fix resetPassphrase() (F6 — latent risk) | Low | Remove if unused, or implement PRAGMA rekey if needed in future. |
| P3 | Refactor SettingsRepository.updateField() dynamic SQL (F12 — design smell) | Low | Replace string interpolation with GRDB query builder or column name whitelist. |
| P3 | Audit UserDefaults usage for sensitive data (F13 — UserDefaults protection) | Low | No action required for current data (non-sensitive). Ensure future UserDefaults usage is audited for sensitive preference storage. |
| P3 | Fix notification identifier type mismatch (F14) | Low | Change scheduleNotification(id:) parameter from Int to Int64 |
| P3 | Update stale appVersion in backup envelope (F15) | Low | Read from Bundle.main.infoDictionary at runtime instead of hardcoding |
| P3 | Replace LIKE '%routine%' tag heuristic (F16) | Low | Use generatedRoutineId column for routine identification; or use exact tag match |
| P3 | Document .public logging justification (F17) | Low | Add comments explaining why .public is used for specific context values |