11 — Security threat model and device-security posture for Rasd Forms
Research note, 2026-08-15. Scope: consolidated threat model + device/app-level security posture for an offline-first React/React Native forms library used on shared, low-end Android phones by rotating enumerators handling PII of vulnerable people. Complements existing notes on encryption-at-rest choices, npm supply chain, ODK-style submission encryption, license-token security and data-protection frameworks.
Method note: the session's WebSearch budget was exhausted before this brief started, so all evidence below comes from direct WebFetch of ~45 primary pages (OWASP, Android/Apple developer docs, Expo SDK 57 docs, ODK docs, SQLite/SQLCipher, Google Play / App Store policy pages, MDN, package READMEs). Anything not fetched today is flagged [unverified].
Summary
- Baseline standards to claim conformance against: OWASP MASVS v2.1.0 (18 Jan 2024) — categories STORAGE, PLATFORM, AUTH, RESILIENCE, PRIVACY — with the MASTG test catalogue as the verification method [1][2][8]; OWASP ASVS 5.0.0 (30 May 2025) for the web/PWA side [10]. Aim for MASVS-L2 storage/platform controls; MASVS-RESILIENCE (root detection, obfuscation) is host-app territory, not the library's.
- Android Auto Backup is on by default (
android:allowBackup="true") and backs up databases, shared prefs andfilesDir(25 MB quota); Android 12+ needsandroid:dataExtractionRuleswith separate<cloud-backup>and<device-transfer>sections;allowBackup=falsemay not stop device-to-device transfer on some Android 12+ devices [11][12]. Expo exposesandroid.allowBackupin app.json;expo-secure-store's config plugin (configureAndroidBackup: trueby default) already injects exclusion rules for its own prefs [22][27]. - iOS: default Data Protection class for third-party app files is Class C (Protected Until First User Authentication); Class A (Complete) makes files unreadable while locked, which breaks background sync [18]. Keychain default is
kSecAttrAccessibleWhenUnlocked(migrates via backup); use…ThisDeviceOnlyvariants for keys/tokens [19].expo-file-system(SDK 57) exposes no API to setNSURLIsExcludedFromBackupKeyor file-protection classes — a config plugin / native module is required [26]. - SQLite
PRAGMA secure_deletedefaults off; without it (or aVACUUM) deleted rows leave forensic traces;FASTmode purges b-tree pages but not freelist pages; FTS shadow tables are not scrubbed [31]. SQLCipher encrypts every page (AES-256-CBC + HMAC-SHA512, PBKDF2 256k iterations) including WAL/journal, and wipes/locks its memory — so crypto-shredding (destroy the key) is the only reliable "secure delete" on any platform [32]. - Browser storage is best-effort unless
navigator.storage.persist()is granted; Safari can evict script-writable storage after 7 days without interaction; eviction is all-or-nothing and not a secure erase [45].Clear-Site-Data: "storage"deletes IndexedDB/SW registrations on logout [46]. expo-screen-capture(SDK 57) sets AndroidFLAG_SECUREand blocks iOS screenshots/recordings; the screenshot listener needsREAD_MEDIA_IMAGESon Android ≤13, a permission Google Play now restricts (enforced 28 May 2025) — do not use the listener [21][13][41].expo-local-authenticationreturns only a boolean; it does not bind a key. MASTG explicitly tests for "event-bound" (non key-bound) biometrics (MASTG-TEST-0266/0327). Key-bound app-lock must go throughexpo-secure-storerequireAuthentication(which becomes unreadable if biometrics are re-enrolled) [22][23][8].- Untrusted form content: DOMPurify 3.4.13 (Apache-2.0/MPL-2.0) is the web sanitizer of record (allow-list tags/attrs,
ALLOWED_URI_REGEXP,RETURN_TRUSTED_TYPE, DOM-clobbering protection); it needs a DOM, so RN needs a different strategy (render markdown to native views, never HTML).react-native-markdown-displayis no longer maintained, opens links viaLinking.openURLand loads http(s)/data images by default [33][35]. - Google Play requires a Permissions Declaration Form + ≤30 s video + prominent in-app disclosure for
ACCESS_BACKGROUND_LOCATION, and Play Console declarations per foreground-service type (TYPE_LOCATION,TYPE_MICROPHONE) for Android 14+ [40][42]; App Store Guideline 2.5.4 limits background modes to their intended purpose and 5.1.5 requires consent before location collection [44]. Background audio recording is a near-certain rejection unless the app is genuinely a recorder — keep audio foreground-only by default. - Vendor packs: no UN-agency vendor questionnaire could be fetched (unverified); use CSA STAR Level 1 / CAIQ v4 as the self-assessment format [48], and prepare a MASTG-based test report plus SBOM. Residual risks that no library can close: root/jailbroken devices, coerced unlock, forensic extraction of a device with a weak PIN, host-app misconfiguration, and malicious server operators.
1. Standards baseline
| Standard | Version / date | Controls relevant to Rasd Forms |
|---|---|---|
| OWASP MASVS | v2.1.0, 2024-01-18 (added MASVS-PRIVACY) [1] | STORAGE-1 "securely stores sensitive data"; STORAGE-2 "prevents leakage of sensitive data" (backups, logs, IPC) [2][3]; PLATFORM-2 "uses WebViews securely" [5]; AUTH-2 "performs local authentication securely" [6]; RESILIENCE-1 "validates the integrity of the platform" [4]; PRIVACY-1 "minimizes access to sensitive data and resources" incl. SDK/SBOM accountability [7] |
| OWASP MASTG | rolling; test IDs listed in [8] | Backups: TEST-0009/0216/0262 (Android), 0058/0215 (iOS). Logs: 0003/0203/0231, 0053/0296/0297. Keyboard cache: 0006/0258, 0055/0313/0314. Pasteboard: 0073/0276–0280. Screenshots: 0010/0289/0291–0294, 0059/0290. Biometrics: 0017/0018/0326–0328, 0064/0266–0271. WebView: 0031–0033, 0077/0078/0376/0377. Root/JB: 0045/0324/0325, 0088/0240/0241 |
| OWASP ASVS | v5.0.0, 2025-05-30 [10] | Web client: V3 session/token handling, V5 validation/sanitization, V14 configuration/CSP (chapter numbers [unverified] against 5.0 renumbering) |
2. Threat model (attacker × asset)
Assets: D drafts, F finalized-unsynced submissions, A attachments (photos/audio/signatures), E entity/dataset lists with beneficiary PII, T audit trails (incl. GPS trail), K keys (DB key, WebCrypto key), C credentials (host bearer token, license token), X form definitions.
| # | Attacker / scenario | Assets | Entry vector | Primary controls (owner) |
|---|---|---|---|---|
| T1 | Lost/stolen phone, screen-locked | D F A E T K C | Physical; adb/fastboot; cloud backup restore on another device | Encrypted DB + Keystore/SecureStore keys (lib); backup exclusion (lib plugin + host manifest); app-lock (lib hook + host UI); purge-after-sync (lib policy) |
| T2 | Second enumerator on shared device | D F A E T | Same app session; browsing "sent" list; exporting | Per-enumerator local profile + logout without data loss (lib); admin-locked settings ODK-style (host, lib exposes flags); redact PII in lists (lib default) |
| T3 | Seized device, forensic extraction (conflict setting) | all | Chip-off / logical extraction; freelist pages; WAL; thumbnails; logs | Crypto-shredding on wipe (lib); secure_delete/VACUUM for plaintext dbs (lib); no PII in logs/crash reports (lib); Class-A/WHEN_PASSCODE_SET for keys (lib default); strong device PIN + MDM (host/agency) — residual |
| T4 | Malicious/compromised server or form author | X → renderer, D | XSS via labels/hints/media URLs; hostile expressions (CPU/memory bombs); oversized datasets; prototype pollution via __proto__ in JSON; malicious deep-link | Sanitizer allow-list (lib MUST); no HTML on RN (lib MUST); expression sandbox with step/time limits (lib MUST); size/row caps + streaming (lib MUST); JSON schema hardening (lib MUST); media allow-list (lib option) |
| T5 | Malicious host page / third-party script (web) | K C D F | Same-origin JS reads IndexedDB, hijacks token in memory | Non-extractable WebCrypto key (lib); token held in memory, not IndexedDB (lib default); CSP/Trusted Types (host DOCUMENT); no unsafe-eval needed (lib) |
| T6 | Network MITM on captive Wi-Fi | C, sync payloads | Cleartext, rogue CA on device, downgrade | HTTPS only, cleartext blocked (lib refuses http: endpoints except localhost dev); optional public-key pinning hook (lib option); user-installed CAs untrusted on API ≥ 24 by default (platform) [14] |
| T7 | Insider with export rights | F A E T | Bulk export to unencrypted file/SD card, sharing intent | Encrypted export bundle only (lib MUST); export gated by admin PIN/host permission (lib option); audit event (lib) |
| T8 | OS backup / cloud leakage | D F A E K C | Android Auto Backup / D2D transfer; iCloud/iTunes backup; Keychain migration | Backup exclusion rules (lib plugin, host applies); ThisDeviceOnly keychain class (lib); iOS isExcludedFromBackup on lib dirs (lib native module) |
| T9 | Leakage side-channels | D E | Logs, crash reporters, clipboard, autofill/keyboard cache, notifications, Recents thumbnail, WebView widgets, intents | Log redaction (lib default); Sentry beforeSend scrubber + attachScreenshot=false (lib helper); FLAG_SECURE on sensitive screens (lib option); importantForAutofill="no", autoCorrect=false on PII fields (lib default); local-only WebView with originWhitelist=[] (lib) |
3. Control matrix (asset × platform)
Legend: MUST = library implements and enables by default; OPT = library implements, host opts in/out; DOC = host responsibility, library documents and (where possible) lints/warns.
| Asset | Web / PWA | Android (RN/Expo) | iOS (RN/Expo) |
|---|---|---|---|
| Drafts / finalized-unsynced (JSON) | MUST: AES-GCM with non-extractable WebCrypto key; MUST: navigator.storage.persist() request; OPT: purge-after-sync; DOC: CSP, no third-party scripts on the form origin | MUST: SQLCipher when available (expo-sqlite useSQLCipher) [25], key in expo-secure-store; MUST: PRAGMA secure_delete=ON on plaintext fallback; OPT: purge-after-sync; DOC: allowBackup=false or dataExtractionRules excluding database/ [11] | MUST: same DB; MUST: isExcludedFromBackup on lib DB/attachment dirs (native module); OPT: file protection Class A vs default C trade-off vs background sync [18]; DOC: com.apple.developer.default-data-protection entitlement [unverified exact key] |
| Attachments (photos/audio/signatures) | MUST: encrypted blobs in IndexedDB or OPFS; MUST: never object-URL cached beyond render; OPT: max size caps | MUST: stored under app-private dir, encrypted file-level (AES-GCM streaming); MUST: no MediaStore/gallery copies unless host opts in; DOC: exclude dir from backup; DOC: FileProvider grants when sharing | MUST: isExcludedFromBackup; MUST: not written to Photos; DOC: NSPhotoLibraryAddUsageDescription only if host opts in |
| Datasets / entity lists (beneficiary PII) | MUST: encrypted; MUST: field-level projection (only columns referenced by forms are downloaded/kept); OPT: TTL + auto-refresh purge | same + MUST: no full-table logging; OPT: search index only over non-PII columns | same |
| Audit trail / GPS trail | MUST: encrypted, capped size, opt-in per form; MUST: never in logs | same; DOC: background location = Play declaration [40] | same; DOC: App Store 5.1.5 consent [44] |
| Host bearer token | MUST: memory only by default; OPT: encrypted IndexedDB with explicit persistToken: true; DOC: refresh flow | MUST: expo-secure-store (Keystore-backed) with keychainAccessible n/a; DOC: values > ~2 KB fail on iOS [22] | MUST: SecureStore WHEN_UNLOCKED_THIS_DEVICE_ONLY [19][22] |
| License token (Ed25519, offline) | MUST: integrity-verified; may live in IndexedDB (not secret) | MUST: verified on load; store in SecureStore or plain DB (public-key verified) | same |
| Encryption keys | MUST: crypto.subtle non-extractable, stored in IndexedDB; DOC: browser profile is the boundary | MUST: SecureStore; OPT: requireAuthentication (biometric-bound; re-enrolment invalidates) [22] | MUST: SecureStore WHEN_PASSCODE_SET_THIS_DEVICE_ONLY OPT (fails without passcode) [19] |
| Form definitions (X) | MUST: sanitized on render; MUST: size cap; MUST: __proto__/constructor keys rejected | same; MUST: markdown → native, no HTML | same |
| Screens showing PII | OPT: blur on visibility-change (best effort) | OPT: preventScreenCaptureAsync per screen [21] | OPT: same + enableAppSwitcherProtectionAsync [21] |
| Logs / crash reports | MUST: structured logger with redaction; MUST: never log answers; OPT: Sentry scrubber helper [38] | same | same |
4. Platform facts (with citations)
4.1 Android
- Auto Backup: default on for API ≥ 23; includes shared prefs,
getFilesDir(), databases,getExternalFilesDir(); excludesgetCacheDir()/getNoBackupFilesDir(); 25 MB/app quota; on Android 9+ backups are E2E-encrypted with device PIN. API ≤ 30 usesandroid:fullBackupContent(<full-backup-content>with<include>/<exclude domain=… path=…>), API ≥ 31 usesandroid:dataExtractionRules(<data-extraction-rules><cloud-backup>…</cloud-backup><device-transfer>…</device-transfer>);requireFlags="clientSideEncryption"restricts to encrypted backups;allowBackup=falsemay not disable D2D transfer on some Android 12+ devices [11]. Android's own risk page recommendsallowBackup=false, exclusion rules, Keystore-encrypted data andgetNoBackupFilesDir()[12]. MASTG-TEST-0216 fails an app that hasallowBackup="true"without restrictive rules [9]. - Expo:
android.allowBackupapp.json property (default true, "useful if your app deals with sensitive information" to set false) [27];expo-secure-storepluginconfigureAndroidBackup(default true) writes exclusion rules for its prefs — the Rasd plugin must merge with, not overwrite, that file [22]. - FLAG_SECURE: "Window content cannot appear in screenshots or on non-secure displays" [13];
expo-screen-captureuses it; MASTG additionally checkssetRecentsScreenshotEnabled(Android 13+) andSurfaceView.setSecure[8][21]. - Network Security Config: cleartext disabled by default API ≥ 28; user CAs untrusted by default API ≥ 24;
<pin-set expiration=…>with mandatory backup pin;<debug-overrides>for dev CAs [14].expo-build-properties(SDK 57) exposesandroid.usesCleartextTraffic,networkInspector, R8 minify flags — no pinning option [24]. Communityreact-native-ssl-public-key-pinning(MIT; OkHttp/TrustKit; Expo dev-build compatible; ≥ 2 SPKI hashes required on iOS) is the practical pinning route [47]. - Enterprise: lock task mode requires a DPC (
setLockTaskPackages), hides status bar/nav, blocks other apps;LOCK_TASK_FEATURE_*flags since 9.0 [15]. Managed configurations:res/xml/app_restrictions.xml+RestrictionsManager.getApplicationRestrictions()+ACTION_APPLICATION_RESTRICTIONS_CHANGED— the right channel for MDM-pushed server URL / project token / policy flags [16]. Dedicated devices support ephemeral users (data deleted on logout/reboot) viacreateAndManageUser(... MAKE_USER_EPHEMERAL)— OS-level answer to shared devices, but optional per OEM [17]. - Keystore:
expo-secure-storevalues are Keystore-encrypted and stored in SharedPreferences, deleted on uninstall;requireAuthenticationdata becomes inaccessible after biometric changes [22].
4.2 iOS
- Data Protection classes A–D; Class C is the default for third-party app data; A and C class keys are discarded on lock (A within 10 s), so Class A files are unreadable during background sync [18].
- Keychain accessibility default
kSecAttrAccessibleWhenUnlocked(migrates via backup);…ThisDeviceOnlyandWhenPasscodeSetThisDeviceOnlydo not migrate [19]. SecureStore items persist across reinstall on iOS [22] — a wipe must explicitly delete them. - Backup exclusion:
NSURLIsExcludedFromBackupKey/isExcludedFromBackupKeyper file/directory; Caches and tmp are never backed up (Apple QA1719 — page not machine-readable today; facts long-standing, [unverified today]) [20].expo-file-systemSDK 57 has no exposed API for it [26], andexpo-sqlitedocs do not address backups [25] → Rasd needs a tiny native module / config plugin. - Screenshots:
expo-screen-captureprevents recordings (iOS 11+) and screenshots (iOS 13+) and offers app-switcher blur [21].
4.3 Web / PWA
- Best-effort storage evicted under pressure (LRU);
persist()upgrades to persistent (Firefox prompts, Chrome/Safari auto-decide); Safari ITP deletes script-writable storage after 7 days without interaction; eviction is whole-origin and not a secure erase [45]. Clear-Site-Data: "storage"runsdeleteDatabaseon every IndexedDB and unregisters service workers — a good server-side logout/wipe backstop [46].- DOMPurify 3.4.13: allow-list config (
ALLOWED_TAGS/ATTR,ALLOWED_URI_REGEXP,FORBID_TAGS),RETURN_TRUSTED_TYPE,SANITIZE_DOM(clobbering) default on; requires a DOM (jsdom in Node; not RN) [33].
4.4 Secure deletion facts
- SQLite:
secure_deletenormally off;ONzero-fills deleted content;FASTscrubs b-tree pages only; otherwiseVACUUMafter delete; FTS3/FTS5 shadow tables may still hold traces [31]. - SQLCipher: page-level AES-256-CBC + per-page HMAC-SHA512, PBKDF2-HMAC-SHA512 256,000 iterations, encrypted WAL/journal/statement journals, memory locked+wiped; file-based temp storage should be disabled [32]. Deleted-but-unvacuumed pages remain ciphertext → destroying the key is sufficient.
- IndexedDB: no API-level secure delete; rely on crypto-shredding of the WebCrypto key record [45].
5. Untrusted form-definition content
- Web renderer:
DOMPurify.sanitize(html, { USE_PROFILES: {html:true}, ALLOWED_TAGS: [p,br,b,strong,i,em,u,s,ul,ol,li,a,span,img,h1-h6,blockquote,code,pre,table,thead,tbody,tr,th,td,sup,sub,bdi,bdo], ALLOWED_ATTR: [href,title,alt,src,dir,lang,class,colspan,rowspan], ALLOWED_URI_REGEXP: /^(?:https?|mailto|tel|data:image\/(?:png|jpeg|webp|gif);base64,)/i, RETURN_TRUSTED_TYPE: true })plus auponSanitizeAttributehook that dropssrcnot matchingmediaAllowListand forcesrel="noopener noreferrer"+target="_blank"[33]. Keepdir/langfor Arabic RTL. Never usedangerouslySetInnerHTMLoutside the sanitized path. - RN renderer: parse markdown (markdown-it,
html: false) to an AST and render nativeText/Image; drop raw HTML tokens; images only frommediaAllowList; links open only via host callback (onOpenLink), neverLinking.openURLimplicitly. Do not adoptreact-native-markdown-display(unmaintained; defaultLinking.openURL; http(s)/data image loading) [35]. - WebView widgets (signature pad): bundle HTML locally,
originWhitelist={[]}/['about:blank'],javaScriptEnabledonly for the bundled asset,allowFileAccess=false,mixedContentMode="never", keepsetSupportMultipleWindows=true, validate everyonMessagepayload as JSON with a schema [34]; prefer a pure-RN/Skia signature pad to avoid WebView entirely. - JSON hardening: parse with a reviver or post-walk that rejects
__proto__,constructor,prototypekeys; useObject.create(null)/Mapfor lookup tables; freeze compiled form definitions [37]. Cap: definition ≤ 2 MB, dataset ≤ 100k rows default, choice list ≤ 10k, string ≤ 64 KB, expression AST ≤ 5k nodes, evaluation budget ≤ 10 ms/step with abort. - Media/URLs:
mediaAllowListdefaults to the host's sync origin; anything else is blocked and reported viaonPolicyViolation.
6. Transport and tokens
- Refuse non-
https:endpoints unlessallowInsecureDevand host islocalhost/10.0.2.2[14][24]. Do not ship a pinning implementation; exposefetchinjection so hosts can plugreact-native-ssl-public-key-pinning[47]. Pinning is worth it only for agencies with stable, agency-controlled certificates; for typical NGO backends behind Cloudflare/managed TLS, pin failures cause field outages — document that trade-off and default off. - Web token: hold in memory; if the host wants offline re-auth it supplies a token provider; never write tokens to localStorage. RN token:
expo-secure-store(≤ ~2 KB on iOS; large JWTs must be split or exchanged for opaque tokens) [22]. - Bearer tokens in sync logs and tus upload URLs must be redacted; tus URLs are capability URLs — treat as secrets.
7. PII leakage vectors checklist
| Vector | Library default | Evidence |
|---|---|---|
| Console/OS logs | Redacting logger; answers/entities never logged; debug level stripped in production builds | MASTG-TEST-0003/0053/0203 [8] |
| Crash reporters | createSentryScrubber() returning beforeSend/beforeBreadcrumb that drops form data; sendDefaultPii=false; attachScreenshot=false (default off, opt-in only, "screenshots may contain PII") | [38][39] |
| Clipboard | PII fields render with contextMenuHidden when field.sensitive; no lib copy-to-clipboard; note expo-clipboard lacks iOS local-only/expiry and Android sensitive flags | [36][28] |
| Autofill / keyboard cache | importantForAutofill="no", autoComplete="off", autoCorrect={false}, textContentType="none" on sensitive fields | [36]; MASTG-TEST-0006/0055 [8] |
| Recents thumbnail / screenshots | preventScreenCaptureAsync while a sensitive screen is mounted (opt-in policy flag) | [21][13] |
| Notifications | Never include answers or names in sync notifications; only counts | MASVS-STORAGE-2 [3] |
| Deep links | Library exposes no deep-link handler; host validates params; form IDs only | MASVS-PLATFORM |
| WebView widgets | Local content only, no bridge except typed messages | [34] |
| Intents / share sheets | Export only through exportEncryptedBundle(); no implicit Sharing | T7 |
| Backups | Plugin writes exclusion rules; iOS native module marks lib dirs excluded | [11][20] |
8. Shared devices, app lock, ODK model
- ODK Collect: admin password protects "Access Control" (hide Get Blank Form / Delete Saved Form / Edit Saved Form / Send Finalized; restrict form-entry navigation) and settings; "Delete after send" removes filled forms after upload [29]. Settings QR codes contain admin and server passwords in plaintext unless stripped — a cautionary model for any QR provisioning Rasd offers [30].
- Rasd model:
profileskeyed by enumerator ID; each profile has its own DB namespace and record ownership;switchProfile()requires PIN/biometric via hostonLock;logout()never deletes unsynced records but hides them from other profiles; supervisor override via admin PIN with audit event. Screen lock after N minutes idle and on app background (autoLockAfterMs). - MDM path: expose managed-configuration keys (
serverUrl,projectToken,policyProfile) read via a host adapter; recommend Android Enterprise ephemeral users or lock-task kiosk for high-risk deployments [16][17][15]. Agency-specific device guidance (UNHCR/WFP) [unverified — not fetched].
9. Remote wipe and purge semantics
- Server signals via sync response header/flag (
X-Rasd-Wipe: <nonce>orpolicy.wipeAt) → library validates nonce against the host token, then: (1) revoke SecureStore/WebCrypto keys (crypto-shred), (2) delete DB files (deleteDatabaseAsync) and attachment dirs, (3) on plaintext fallback DBs runsecure_delete=ON+VACUUMbefore delete, (4) clear profiles, (5) emitonRemoteWipe({ reason, unsyncedCount })— the host decides UX. Web addsClear-Site-Datafrom the server [46][25][31]. - Purge-after-sync default: finalized records + attachments removed after server ACK; drafts kept; "sent" list shows metadata only (no answers) — mirrors ODK "delete after send" [29].
- Local wipe when unsynced data exists must be a two-step confirmation with count shown; never silent.
10. Recommended defaults and API sketch
type SecurityPolicy = {
encryption: { atRest: 'required' | 'preferred' | 'off' /* default 'preferred' */; keyAuth: 'none' | 'biometric' /* default 'none' */ };
backup: { excludeFromOsBackup: boolean /* true */; excludeFromDeviceTransfer: boolean /* true */ };
retention: { purgeFinalizedAfterSync: boolean /* true */; draftMaxAgeDays?: number /* 30 */; datasetTtlHours?: number /* 168 */; auditTrail: 'off' | 'events' | 'events+gps' /* 'events' */ };
lock: { autoLockAfterMs: number /* 300000 */; lockOnBackground: boolean /* true */; requireForExport: boolean /* true */ };
screen: { preventCaptureOn: 'none' | 'sensitiveFields' | 'all' /* 'sensitiveFields' */ };
content: { markdown: 'safe' | 'off' /* 'safe' */; mediaAllowList: string[] /* [syncOrigin] */; maxDefinitionBytes: number /* 2 MiB */; maxDatasetRows: number /* 100000 */; exprBudgetMs: number /* 10 */ };
transport: { requireHttps: boolean /* true */; fetch?: typeof fetch /* pinning hook */ };
tokens: { persistHostToken: boolean /* false on web */ };
logging: { level: 'error'|'warn'|'info'|'debug' /* 'warn' */; redactor?: (rec) => rec };
export: { mode: 'encrypted-only' /* only mode */; recipientKeys?: string[] };
profiles: { enabled: boolean /* true */; adminPinHash?: string };
};
// events
onLock(reason: 'idle'|'background'|'switch'|'policy'); onUnlockRequired(next: () => void);
onRemoteWipe({ reason, unsyncedCount }); onLocalWipeRequested({ unsyncedCount, confirm });
onPolicyViolation({ kind: 'html'|'url'|'size'|'expr'|'proto'|'transport', detail });
onSecureStoreUnavailable({ fallback: 'plaintext-db' | 'refuse' }); onBackupExclusionMissing(platform);
Startup self-check (rasd.securityReport()) returns which controls are active (SQLCipher on?, backup rules present?, iOS exclusion applied?, https?, sanitizer profile) for the vendor pack and for the host's CI.
11. Store policy checklist (location / audio / camera / media)
| Feature | Google Play | App Store |
|---|---|---|
| Foreground GPS on question | Runtime ACCESS_FINE_LOCATION; Data safety "Location" declared [43] | NSLocationWhenInUseUsageDescription; 5.1.5 consent [44] |
| Background location trail (audit) | ACCESS_BACKGROUND_LOCATION only for core feature; Permissions Declaration Form (single feature), prominent disclosure with the word "location" and "when the app is closed", ≤30 s video, privacy policy URL [40]; Android 14+ FOREGROUND_SERVICE_LOCATION type declaration + video [42] | 5.1.5 + Background Modes only for intended purpose (2.5.4) [44]; expect review scrutiny — ship as opt-in module, off by default |
| Audio recording | RECORD_AUDIO; background needs FOREGROUND_SERVICE_MICROPHONE declaration ("voice recording" listed as acceptable) [42] | Foreground only by default; background audio recording under 2.5.4 is a common rejection [unverified statistic] |
| Camera / photos | Use system Photo Picker; do not request READ_MEDIA_IMAGES/VIDEO unless core (enforced 28 May 2025) — so no expo-screen-capture screenshot listener [41] | NSCameraUsageDescription; only add Photos usage strings if host saves to library |
| Data safety / privacy labels | Declare SDK-originated collection; local-only processing is not "collection"; deletion mechanism or auto-deletion within 90 days [43] | 5.1.1 privacy policy + purpose strings; 5.1.2 no repurposing [44] |
12. Verification plan
- MASTG automation (host CI, lib provides scripts): 0216/0262 (parse merged manifest + XML rules; fail if DB/attachment dirs not excluded), 0215 (iOS: assert exclusion attribute on lib dirs via XCTest helper), 0203/0231 (grep release bundle for
console.*/Log.calls in lib code), 0258/0313 (static: sensitive inputs carry autofill/keyboard-cache props), 0291–0294 (static:FLAG_SECUREAPI references present), 0326–0328/0266–0271 (biometric key-binding path uses SecureStorerequireAuthentication, not bare LocalAuthentication) [8][9]. - Static checks in the lib repo: ESLint rules banning
dangerouslySetInnerHTML,eval/new Function,Linking.openURL,console.logoutside logger; type test thatSecurityPolicydefaults match this note; fuzz tests for the expression sandbox (time/step budget), JSON prototype-pollution corpus, DOMPurify bypass corpus (mXSS vectors). - Runtime evidence:
securityReport()JSON, SQLCipherPRAGMA cipher_versioncheck, backup restore test on Android emulator (bmgr backupnow→ reinstall → assert no DB), iOS backup exclusion viaxcrun simctlfile attribute dump. - Vendor pack (see §13) built from these outputs.
13. UN vendor security pack (content list)
No UNHCR/WFP vendor questionnaire could be fetched (unverified); structure the pack around CSA STAR Level 1 / CAIQ v4 (self-assessment against CCM v4; Valid-AI-ted review $595, free for CSA members) [48], with HECVAT Lite and SIG Lite as alternate formats [versions unverified]. Contents: (1) architecture & data-flow diagram (device ↔ host server; the vendor never receives beneficiary data — only license telemetry); (2) data inventory per asset class with encryption/retention defaults; (3) MASVS 2.1 mapping table with MASTG evidence; (4) ASVS 5.0 mapping for the web bundle; (5) SBOM (CycloneDX) + provenance attestations from OIDC publishing (see existing supply-chain note); (6) DPIA template for host agencies (lawful basis under UN PDPP, data minimization defaults, retention, wipe procedure); (7) vulnerability disclosure policy + SLA; (8) SOC 2 Type 1 scope statement covering only the license service and update distribution (device data out of scope, since it never transits vendor systems); (9) store-policy compliance sheet (§11); (10) residual-risk register (§14).
14. Residual risks (cannot be closed by a library)
- Rooted/jailbroken or malware-infected devices: Keystore/SecureStore, FLAG_SECURE and sandboxing are all bypassable; only host-side root detection (MASVS-RESILIENCE) and MDM attestation reduce this [4].
- Coerced unlock / weak or shared device PIN: on-device encryption is bound to the device lock; enumerators sharing a PIN defeats T1/T3.
- Forensic recovery of freed pages / thumbnails / keyboard learning data outside the app sandbox.
- Host misconfiguration:
allowBackup=truewithout rules, tokens in localStorage, missing CSP, disablingpurgeFinalizedAfterSync— mitigated only by warnings andsecurityReport(). - Malicious server operator: can push forms that collect more PII than needed; sanitizer prevents code execution, not over-collection.
- Web platform limits: no key-bound biometrics, storage eviction, browser extensions with page access.
- Loss of unsynced data on wipe/eviction is itself a protection risk (beneficiary interviews repeated) — a trade-off the host must own.
Implications & recommendations for Rasd Forms
- Ship a
@rasd/forms-expoconfig plugin that (a) setsallowBackup=falseor writesdataExtractionRulesexcludingdatabase/and the attachments dir for both cloud-backup and device-transfer, merging withexpo-secure-storerules; (b) adds a small iOS native module that setsisExcludedFromBackupon lib dirs at first run; (c) optionally sets the Data Protection entitlement (document Class A vs background-sync trade-off) [11][22][18][26]. - Default
encryption.atRest='preferred'(SQLCipher viauseSQLCipher, key in SecureStoreWHEN_UNLOCKED_THIS_DEVICE_ONLY; web AES-GCM non-extractable) and emitonSecureStoreUnavailablerather than silently falling back [25][22][19]. - Treat crypto-shredding as the wipe primitive; add
secure_delete=ON+VACUUMonly for plaintext fallback DBs; exposewipeLocal()and server-triggeredonRemoteWipe[31][32]. - Purge finalized submissions and attachments after ACK by default; keep only metadata for the "sent" view [29].
- Provide
createSentryScrubber()and a redacting logger; documentattachScreenshotstays false [38][39]. - Sanitize with DOMPurify on web (allow-list above, Trusted Types) and render markdown-to-native on RN; never render HTML in RN; enforce media allow-list and size/expr budgets; reject prototype-polluting keys [33][35][37].
- App-lock: expose
onLock/onUnlockRequiredand a key-bound option using SecureStorerequireAuthentication; document that bareexpo-local-authenticationis UX-only (fails MASTG event-bound tests) [22][23][8]. - Do not implement TLS pinning in-core; accept a custom
fetchand documentreact-native-ssl-public-key-pinning; hard-fail onhttp:outside dev [14][47]. - Keep background location trail and audio recording as separately-installed optional modules with store-declaration templates included; default foreground-only [40][42][44].
- Never call the screenshot listener API (needs
READ_MEDIA_IMAGES≤ Android 13, Play-restricted) [21][41]. - Implement enumerator profiles with logout-without-loss and admin PIN, and read Android managed configurations for MDM-pushed settings; publish an "ODK-style admin lock" recipe [16][29][30].
- Publish
securityReport(), MASTG mapping, SBOM, CAIQ v4 self-assessment and a DPIA template as the vendor pack; state SOC 2 scope as license/update services only [48].
Sources (accessed 2026-08-15)
- OWASP MASVS releases — https://github.com/OWASP/owasp-masvs/releases
- MASVS-STORAGE-1 — https://raw.githubusercontent.com/OWASP/owasp-masvs/master/controls/MASVS-STORAGE-1.md
- MASVS-STORAGE-2 — https://raw.githubusercontent.com/OWASP/owasp-masvs/master/controls/MASVS-STORAGE-2.md
- MASVS-RESILIENCE-1 — https://raw.githubusercontent.com/OWASP/owasp-masvs/master/controls/MASVS-RESILIENCE-1.md
- MASVS-PLATFORM-2 — https://raw.githubusercontent.com/OWASP/owasp-masvs/master/controls/MASVS-PLATFORM-2.md
- MASVS-AUTH-2 — https://raw.githubusercontent.com/OWASP/owasp-masvs/master/controls/MASVS-AUTH-2.md
- MASVS-PRIVACY-1 — https://raw.githubusercontent.com/OWASP/owasp-masvs/master/controls/MASVS-PRIVACY-1.md
- OWASP MASTG tests index — https://mas.owasp.org/MASTG/tests/
- MASTG-TEST-0216 — https://mas.owasp.org/MASTG/tests/android/MASVS-STORAGE/MASTG-TEST-0216/
- OWASP ASVS releases (v5.0.0, 2025-05-30) — https://github.com/OWASP/ASVS/releases
- Android Auto Backup — https://developer.android.com/identity/data/autobackup
- Android security risk: backup leaks — https://developer.android.com/privacy-and-security/risks/backup-leaks
- Android
WindowManager.LayoutParams#FLAG_SECURE— https://developer.android.com/reference/android/view/WindowManager.LayoutParams#FLAG_SECURE - Android Network Security Configuration — https://developer.android.com/privacy-and-security/security-config
- Android lock task mode — https://developer.android.com/work/dpc/dedicated-devices/lock-task-mode
- Android managed configurations — https://developer.android.com/work/managed-configurations
- Android multiple users on dedicated devices — https://developer.android.com/work/dpc/dedicated-devices/multiple-users
- Apple Platform Security: Data Protection classes — https://support.apple.com/guide/security/data-protection-classes-secb010e978a/web
- Apple: Restricting keychain item accessibility — https://developer.apple.com/documentation/security/keychain_services/keychain_items/restricting_keychain_item_accessibility
- Apple QA1719 /
isExcludedFromBackupKey(page not machine-readable at fetch time; unverified today) — https://developer.apple.com/library/archive/qa/qa1719/_index.html - Expo ScreenCapture (SDK 57) — https://docs.expo.dev/versions/latest/sdk/screen-capture/
- Expo SecureStore (SDK 57) — https://docs.expo.dev/versions/latest/sdk/securestore/
- Expo LocalAuthentication (SDK 57) — https://docs.expo.dev/versions/latest/sdk/local-authentication/
- Expo BuildProperties (SDK 57) — https://docs.expo.dev/versions/latest/sdk/build-properties/
- Expo SQLite (SDK 57) — https://docs.expo.dev/versions/latest/sdk/sqlite/
- Expo FileSystem (SDK 57) — https://docs.expo.dev/versions/latest/sdk/filesystem/
- Expo app config reference (
android.allowBackup) — https://docs.expo.dev/versions/latest/config/app/ - Expo Clipboard (SDK 57) — https://docs.expo.dev/versions/latest/sdk/clipboard/
- ODK Collect settings / Access Control — https://docs.getodk.org/collect-settings/
- ODK Collect settings QR import/export — https://docs.getodk.org/collect-import-export/
- SQLite
PRAGMA secure_delete— https://www.sqlite.org/pragma.html#pragma_secure_delete - SQLCipher design — https://www.zetetic.net/sqlcipher/design/
- DOMPurify README (v3.4.13) — https://github.com/cure53/DOMPurify
- react-native-webview Reference — https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md
- react-native-markdown-display README (unmaintained notice) — https://github.com/iamacup/react-native-markdown-display
- React Native TextInput (0.87) — https://reactnative.dev/docs/textinput
- OWASP Prototype Pollution Prevention Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Prototype_Pollution_Prevention_Cheat_Sheet.html
- Sentry React Native: sensitive data — https://docs.sentry.io/platforms/react-native/data-management/sensitive-data/
- Sentry React Native: screenshots — https://docs.sentry.io/platforms/react-native/enriching-events/screenshots/
- Google Play: location permissions policy — https://support.google.com/googleplay/android-developer/answer/9799150
- Google Play: photo and video permissions policy — https://support.google.com/googleplay/android-developer/answer/14115180
- Google Play: foreground service permissions (Android 14+) — https://support.google.com/googleplay/android-developer/answer/13392821
- Google Play: Data safety section — https://support.google.com/googleplay/android-developer/answer/10787469
- Apple App Store Review Guidelines (2.5.4, 5.1.1, 5.1.2, 5.1.5) — https://developer.apple.com/app-store/review/guidelines/
- MDN: Storage quotas and eviction criteria — https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria
- MDN: Clear-Site-Data — https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Clear-Site-Data
- react-native-ssl-public-key-pinning — https://github.com/frw/react-native-ssl-public-key-pinning
- CSA STAR / CAIQ v4 — https://cloudsecurityalliance.org/star
Not retrievable today (403/404/auth-walled), flagged unverified where referenced: EDUCAUSE HECVAT, Shared Assessments SIG, Apple FileProtectionType/isExcludedFromBackupKey developer pages, AndroidX EncryptedSharedPreferences reference, markdown-it docs/security.md, UNHCR/WFP vendor security questionnaires.