Skip to content

[nightshift] Security Audit: plaintext session storage, tamperable auth state #14

Description

@nightshift-micr

[nightshift] Security Audit: Ancla

Codebase: Microck/ancla
Date: 2026-04-07
Auditor: Nightshift v3 (GLM 5.1) — automated static analysis
Scope: All Swift source files, entitlements, plists, CI workflows


Summary

Ancla is an iOS app that uses NFC physical tags as keys to lock and release Screen Time / app blocking sessions. It uses Apple's FamilyControls and ManagedSettings frameworks. No network communication occurs — this eliminates a large class of vulnerabilities. The primary attack surface is local data at rest and the integrity of stored session state.


Findings

1. Plaintext Storage of Sensitive Session State — P1 High

Files: ios/ancla-shared/ancla-store.swift:36-40, ios/ancla-lite/ancla-lite-support.swift:67-70
Category: Insecure data storage

The AppSnapshot — containing NFC tag hash fingerprints (uidHash), session authorization state (isAuthorized), emergency unbrick count, and full session history — is serialized as unencrypted, plaintext JSON to disk. Pretty-printed with sorted keys, making it easy to read and modify.

Fix: Encrypt using CryptoKit AES-GCM with a Keychain-stored key. Add NSFileProtectionComplete. Remove .prettyPrinted in production.


2. Emergency Unbrick Counter Tamperable — P1 High

Files: ios/ancla-shared/ancla-models.swift:212, ios/ancla-shared/ancla-store.swift:36-40
Category: Authorization bypass

emergencyUnbricksRemaining (default: 5) is a plain integer in the JSON snapshot. With filesystem access, the counter can be reset to any value, bypassing the physical NFC anchor requirement entirely.

Fix: Store in iOS Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly. Add HMAC over the snapshot.


3. No Integrity Verification on Stored Snapshot — P1 High

Files: ios/ancla-shared/ancla-store.swift:26-33, ios/ancla-lite/ancla-lite-support.swift:57-65
Category: Integrity / Tampering

Snapshot is decoded with JSONDecoder without integrity check. Attacker with write access could modify isAuthorized, activeSession.state, pairedTagId, or remove session history entries.

Fix: Compute HMAC-SHA256 using a Keychain-stored key. Verify before decoding. Reset on verification failure.


4. Shield Extension Reads Shared State Without Synchronization — P2 Medium

Files: ios/ancla-shield-extension/shield-configuration-extension.swift:6,31
Category: Race condition

ShieldConfigurationExtension instantiates its own AppGroupStore() and calls load() independently on every callback. No file locking or coordination with the main app's writes.

Fix: Use NSFileCoordinator/NSFilePresenter, or add a version/timestamp field.


5. NFC Tag Hash Partially Exposed in UI — P2 Medium

Files: ios/ancla-app/content-view.swift:1138-1142
Category: Privacy leak

First 4 and last 4 characters of the NFC tag's SHA-256 hash displayed in UI. Shoulder surfing or screenshots leak 32 bits of the authentication credential.

Fix: Replace with a non-reversible color swatch or shortened 3-char preview.


6. Session History Grows Without Bound — P2 Medium

Files: ios/ancla-shared/ancla-core.swift:138-160
Category: Data exposure / Resource exhaustion

sessionHistory entries appended with no pruning. Full history persists in plaintext JSON indefinitely.

Fix: Cap at 100 entries. Prune oldest on overflow.


7. Test Hash Injection via Environment Variable — P2 Medium

Files: ios/ancla-lite/ancla-lite-support.swift:30-46,100-111
Category: Authentication bypass in non-production builds

ANCLA_TEST_STICKER_HASHES env var injects arbitrary NFC tag hashes, completely bypassing the physical token. Debug builds on real devices are vulnerable.

Fix: Gate behind #if DEBUG in addition to env var check. Add runtime assertion for Release builds.


8. Sideload-Lite Build Bypasses All Authorization — P2 Medium

Files: ios/ancla-lite/ancla-lite-support.swift:86-90, ios/ancla-app/app-view-model.swift:201-204
Category: Authorization bypass by design

Sideload-lite build has no actual app blocking — LiteAuthorizationClient.request() is a no-op, auto-sets isAuthorized = true. Users may misunderstand the build variant.

Fix: Prominently distinguish lite build as "simulation only" in UI.


9. No File Protection Attributes on Stored Snapshot — P2 Medium

Files: ios/ancla-shared/ancla-store.swift:39, ios/ancla-lite/ancla-lite-support.swift:70
Category: Insecure data storage

Snapshot written without NSFileProtectionComplete. Files may be accessible when device is locked via forensic tools.

Fix: Set FileProtectionType.complete after writing.


10. App Group Identifier Hardcoded — P3 Low

Files: ios/ancla-shared/ancla-models.swift:4
Category: Information disclosure

group.dev.micr.ancla hardcoded in source. Low risk due to Apple provisioning, but any app can read on jailbroken devices.


11. StickerPairingService Continuation Edge Case — P3 Low

Files: ios/ancla-services.swift:72-177
Category: Memory safety

Manual CheckedContinuation management has theoretical edge case where post-invalidation processing could attempt to invalidate an already-invalidated NFC session.

Fix: Add explicit isFinished boolean guard.


Positive Findings

  • CI workflows properly configured: secrets via secrets.*, least-privilege permissions, chmod 600 on API keys, concurrency groups.
  • Zero network communication: no analytics, telemetry, or API calls. Eliminates MITM, data exfiltration, and SSL/TLS vectors.
  • No hardcoded secrets found in the repository.

Severity Summary

Severity Count Finding IDs
P0 Critical 0 —
P1 High 3 #1, #2, #3
P2 Medium 5 #4, #5, #6, #7, #8, #9
P3 Low 2 #10, #11

Top Priority

Encrypt the snapshot at rest — addresses findings #1, #2, #3, and partially #5 and #9 in a single fix. Use CryptoKit AES-GCM with Keychain-stored key + HMAC integrity verification.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions