TailSpend Docs

Security remediation — 13 September 2026

This change addresses TS-01 through TS-07 in the assessment. No paid OCR/model calls were made during local verification. Release status is tracked by the Main Pipeline run for the merged commit.

Fixes

Finding Implemented control
TS-01: PDF and processing amplification Read at most 10 MiB plus one overflow byte; verify the actual SHA-256 and image signature/PDF page count; reject encrypted, invalid, or over-three-page PDFs before Textract. Reserve daily processing starts before parsing and page allowances before OCR. Limit model input to 24,000 OCR characters and output to 2,048 tokens. Disable automatic paid-provider retries.
TS-02: signup-credit recycling Atomically write a purpose-separated HMAC fingerprint with the first signup grant. It has neither a UserId index entry nor a TTL, so deletion cannot erase eligibility. Recreated accounts receive zero signup credits. Deleting legacy accounts first records fingerprints for their identity mappings before removing those mappings.
TS-03: unauthenticated reads and request amplification Require a valid signed invitation for every public group/expense read and join request. Reject forged capabilities before group reads. Rate-limit public source IPs and authenticated subjects; use conditional increments, fail closed on limiter failures, and cache denials in warm workers. Add API Gateway route throttles and DynamoDB table/index throughput ceilings.
TS-04: forced membership and deletion veto Reject direct USER participant additions. Joining an account requires that user's signed-link request and owner approval. Self-departure/account deletion detaches identity into a guest record, preserving expense and settlement references. Sole owners of shared groups must still transfer ownership first.
TS-05: unlimited upload storage Require available scan credit and reserve both upload count and bytes against per-user and global daily allowances before presigning. Bind PUTs to length and If-None-Match: *, with a signed checksum for hash-based uploads; uploaded objects cannot be overwritten through these tickets.
TS-06: unbounded fan-out and payloads Unchanged profile names cause no writes. Weight profile/dashboard requests more heavily; cap creation, rosters, balance reads, comments, items, payments, divisions, and request-body size. Snapshot a bounded membership set before profile fan-out. Oversized history errors do not masquerade as zero balances.
TS-07: replay after job TTL A previously charged attempt whose job is gone returns 410 and cannot recreate or enqueue work. Duplicate queue deliveries require an atomic processing lease; at most two worker attempts are allowed per charged run.

Recurring templates now record their actual creator, and the worker pauses templates when that participant loses write access. Payers no longer confer ownership. Legacy templates with no trustworthy creator are paused rather than automatically attributed to a payer.

Electron now uses a sandboxed preload, rejects non-app navigation and untrusted auth IPC senders, opens only HTTPS external links, validates authentication destinations/deep links, and avoids plaintext token persistence when OS encryption is unavailable.

Invitation behavior

Owners retrieve signed links through GET /groups/{groupId}/invite/settings. The token contains the group ID, purpose, revision and expiry, authenticated with HMAC-SHA256. It expires after 90 days. Enabling or replacing a link creates a new random revision; disabling or replacing invalidates previously saved links through a consistent server-side revision check.

A token grants the read-only invite view, whether the visitor is signed in or signed out. Joining as a participant still requires consent and owner approval. Web/desktop use versioned localStorage keys; native mobile uses SecureStore. Returning to the group's invite route restores its saved capability. API calls carry the token in x-tailspend-invite, not API query strings. Web referrer policy is no-referrer, and API responses use no-store.

These are bearer links: anyone who receives a forwarded copy can use it until expiry or revocation. Tokens do not prove the recipient's identity. Stored tokens remain exposed to code running in the same web origin; persistent storage does not replace XSS defenses. Revocation blocks future reads but cannot retract data already viewed or copied.

Initial operating limits

Limits are deliberately conservative and can be changed in the reviewed configuration/code as usage grows.

Resource Per user Per service/stage
Upload tickets per UTC day 50 2,000
Declared upload bytes per UTC day 150 MiB 10 GiB
Worker processing starts per UTC day, including retries/invalid documents 50 1,000
OCR pages per UTC day 150 3,000
Request allowance per minute 120 weighted units Gateway route limits below

Each receipt is at most 10 MiB and three pages. Processing starts are reserved before PDF parsing, so malformed inputs also consume an allowance. Failed work does not replenish an allowance. Daily quota rows expire after three days; persistent creation counters do not expire.

Creation budgets: 50 membership creations per account; 49 additional participant creations per group (plus its initial owner); 1,000 expenses, 1,000 settlements, and 50 recurring templates per group; 100 comments per expense. Deletion does not restore creation allowance. Balance aggregation is capped at five pages of up to 200 expense/settlement rows; roster and name fan-out reads are also bounded. Requests above 256 KiB are rejected.

Gateway defaults are 30 requests/second with a 60-request burst per route, reduced to 2/5 for upload/job creation and 5/10 for public reads and ad callbacks. Tables and GSIs have 500 read-unit / 200 write-unit ceilings. These controls bound application work; they are not an absolute AWS dollar cap, and rejected traffic, requests, logs, storage, and other infrastructure can still incur charges.

Rollout requirements

Verification and dependency status

Service tests cover token tampering, cross-group reuse, expiry, revocation, missing tokens, PDF limits, allowance failures, upload credit checks, TTL replay, duplicate worker deliveries, departure with history, creator revocation, unchanged-name suppression, and handler rate/body limits. Client tests exercise persistence and capability headers across preview and authenticated join requests. Provisioning tests verify both atomic first grants and zero-credit recreation.

Passing suites include 941 service tests, 148 shared UI tests, 34 infrastructure tests, 17 desktop Vitest tests, and 10 mobile Node tests. Local verification also includes TypeScript checks, lint, a clean-install dry run, OpenAPI/client generation, desktop builds and launch with sandbox: true, contextIsolation: true, nodeIntegration: false, and a working preload bridge. An iOS production export succeeded. Its source map contains the patched decoder adapter and decode-uri-component-safe, and excludes the vulnerable decoder implementation. No live AWS integration/deployment or native device sign-in was performed.

Compatible dependency updates are included. Expo SDK 56 still declares the vulnerable CommonJS URL-decoder chain; Metro redirects it through a compatibility adapter to upstream decode-uri-component 0.5.0, retaining Expo's module contract. See the upstream advisory. Automated audit output can still flag this installed but replaced chain.

The Metro image-size dependency has advisories with no available patched image-size release in the audit: ICNS parser and JXL/HEIF parsers. These concern build-time asset parsing; receipt uploads are processed by the services pipeline, not Metro. Do not treat the dependency audit as clean. The final audit reports seven entries (four high, three moderate, zero critical), including transitive parent packages. Remaining entries and exact versions are captured in the post-remediation audit. All installed js-yaml copies are updated to 4.3.2.

The original reproduce-2026-09-12.mjs is historical evidence for the assessment's baseline commit, not a passing regression script for this fixed tree. Use the current service and client test suites to verify these controls.

Release preflight

On 13 September, a bounded read-only inventory completed for all three stages. Production had 39 groups, at most four participants and 33 expenses per group, and at most two memberships per account; no recurring templates were found. Dev and beta were also below the new read limits. No legacy data migration is required for these observed sizes. Production has 39 enabled unsigned invite records; owners must replace their links after release. All three signing parameters exist and contain 64 bytes; their values were not logged or changed.

The change is rebased onto the current async Lambda scan dispatcher, preserving the removal of idle SQS polling. Worker leases and attempt limits apply to direct invocation and retained legacy SQS messages. Live integration tests now send signed invite capabilities and immutable upload headers, check missing/forged/revoked invitations, and back off once on explicit 429 responses. Persistent failures still fail the release gate.

Follow-up issues

Resolved on 14 September 2026, in the same release:

Still open:

View Markdown source