Summary
The Admin-gated import action reads storageKey straight from the submitted form and enqueues an import that reads it via getFileBuffer. There is no check that the key is prefixed with the caller's ${congregationId}/, and the local storage driver's path.join(LOCAL_STORAGE_ROOT, key) has no traversal guard. An admin can submit ../../<path> or another tenant's key. (Requires Admin, the file must parse as a .unitae ZIP, and legitimate keys are UUIDs → Medium.)
Affected code
app/features/settings/routes/congregation/import-confirm.tsx (~L190-200)
app/shared/infra/file-storage.server.ts (~L87, 160)
Remediation
- Validate
storageKey against ^${user.congregationId}/imports/[0-9a-f-]+\.unitae$.
- In the storage layer, resolve the final path and assert it stays within
LOCAL_STORAGE_ROOT.
- Apply the same ownership check to the import status/confirm routes (shares the export IDOR pattern).
Severity
Medium.
Source: internal security review (commit 0360e87, 2026-07-21).
Summary
The Admin-gated import action reads
storageKeystraight from the submitted form and enqueues an import that reads it viagetFileBuffer. There is no check that the key is prefixed with the caller's${congregationId}/, and the local storage driver'spath.join(LOCAL_STORAGE_ROOT, key)has no traversal guard. An admin can submit../../<path>or another tenant's key. (Requires Admin, the file must parse as a.unitaeZIP, and legitimate keys are UUIDs → Medium.)Affected code
app/features/settings/routes/congregation/import-confirm.tsx(~L190-200)app/shared/infra/file-storage.server.ts(~L87, 160)Remediation
storageKeyagainst^${user.congregationId}/imports/[0-9a-f-]+\.unitae$.LOCAL_STORAGE_ROOT.Severity
Medium.
Source: internal security review (commit
0360e87, 2026-07-21).