fix(security): harden sidecar auth, bump deps, validate contactMessages#3388
fix(security): harden sidecar auth, bump deps, validate contactMessages#3388svanvliet wants to merge 1 commit into
Conversation
Addresses items from internal security review: - C2 protobufjs RCE (GHSA-xq3m-2v4x-88gg, CVSS 9.8) — add a top-level npm override forcing protobufjs ^7.5.5, eliminating the vulnerable 6.11.4 pinned transitively by onnx-proto via @xenova/transformers. - H1 sidecar fail-open auth (src-tauri/sidecar/local-api-server.mjs) — previously, an unset LOCAL_API_TOKEN silently disabled the auth gate, turning any standalone sidecar (Docker mode, manual launch) into an open local-HTTP proxy. Now default-deny: missing token returns 503 with a clear log line. The Tauri Rust shell already sets the token at launch, so production behaviour is unchanged. - H2 dompurify advisories (GHSA-39q2-94rc-95cp, GHSA-h7mw-gpvr-xq4m) — bump dompurify ^3.1.7 → ^3.4.0 (lockfile resolves to 3.4.x). The widget-sanitizer iframe sandbox remained a defence-in-depth layer. - M2 contactMessages.submit DoS surface — the Convex public mutation had no length bounds, no email validation, and no throttle. Add RFC-aligned max lengths, email shape check, control-char strip, and a per-normalized-email rate limit (5 / 1h via a new index). Edge-side validation in submit-contact.ts is unchanged; this is defence-in-depth against direct Convex client calls. Tests: - New convex/__tests__/contactMessages.test.ts (7 cases) covers validation, clipping, control-char stripping, and rate-limit bucketing (incl. case-insensitive normalization). - New default-deny regression test in local-api-server.test.mjs. - Updated existing sidecar tests to use a shared authFetch helper now that the global gate is active. Verification: npm run typecheck, typecheck:api, test:convex (90/90), test:sidecar (only pre-existing 'strips browser origin headers' flake remains, unrelated to these changes), npm audit confirms protobufjs critical and dompurify moderate advisories are resolved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@svanvliet is attempting to deploy a commit to the World Monitor Team on Vercel. A member of the Team first needs to authorize it. |
Greptile SummaryThis PR closes four security findings from an internal audit: a Confidence Score: 4/5Safe to merge; all findings are P2 style/clarification issues with no functional defects. All changes are well-tested and the logic is correct. The only issues found are a misleading test comment and a minor schema note that pre-migration rows without convex/schema.ts — the optional Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant Sidecar as local-api-server.mjs
participant ConvexMutation as contactMessages.submit
participant DB as Convex DB
Client->>Sidecar: HTTP request (any route)
alt LOCAL_API_TOKEN unset/empty
Sidecar-->>Client: 503 Service misconfigured
else Bearer token missing or wrong
Sidecar-->>Client: 401 Unauthorized
else Token valid
Sidecar-->>Client: Route handler response
end
Client->>ConvexMutation: submit({name, email, ...})
ConvexMutation->>ConvexMutation: clip() — strip ctrl chars, truncate fields
alt name blank after clip
ConvexMutation-->>Client: ConvexError("Name is required")
else email invalid shape
ConvexMutation-->>Client: ConvexError("Valid email is required")
else valid
ConvexMutation->>DB: query by_normalized_email_received index (last 1h)
alt >= 5 recent submissions
ConvexMutation-->>Client: ConvexError({kind:"rate_limited"})
else under limit
ConvexMutation->>DB: insert contactMessages row
ConvexMutation-->>Client: {status:"sent"}
end
end
Reviews (1): Last reviewed commit: "fix(security): harden sidecar auth, bump..." | Re-trigger Greptile |
| normalizedEmail: v.optional(v.string()), | ||
| }).index("by_normalized_email_received", ["normalizedEmail", "receivedAt"]), |
There was a problem hiding this comment.
normalizedEmail optional in schema — pre-existing rows silently bypass rate-limit
normalizedEmail is declared v.optional(v.string()), so records written before this migration have no value for the field. The rate-limit query filters on .eq("normalizedEmail", normalizedEmail), which won't match those rows. For a newly deployed system this is harmless, but if the table already contains many rows for a given address (e.g. from an earlier spam burst) those rows won't count toward the 5-per-hour cap, effectively resetting the window for every address that existed before this PR.
If the team is comfortable with this one-time gap, a comment explaining the intentional trade-off would help future readers. Alternatively, backfilling normalizedEmail on existing rows closes the window entirely.
| // Request without auth header should be rejected | ||
| const response = await fetch(`http://127.0.0.1:${port}/api/local-env-update`, { | ||
| const response = await authFetch(`http://127.0.0.1:${port}/api/local-env-update`, { |
There was a problem hiding this comment.
Misleading "without auth header" comment — actually tests wrong-token rejection
authFetch unconditionally injects Authorization: Bearer <TEST_LOCAL_API_TOKEN> when the options don't already include an Authorization key. Because this test starts the server with a different token ('secret-token-123'), the first request will be rejected with 401 for supplying the wrong token, not for a missing header. The assertion and test outcome are still correct, but the comment misdescribes what is being verified and could mislead future contributors into believing bare unauthenticated requests are covered here. Consider using a plain fetch (no auth header at all) for this leg and updating the comment to reflect that.
Addresses one Critical, two High, and one Medium item from the recent internal security review.
Fixes
protobufjsRCE (GHSA-xq3m-2v4x-88gg, CVSS 9.8) reachable through@xenova/transformers→onnx-protoLOCAL_API_TOKENfail-open: unset token silently disabled the auth gate, turning any standalone sidecar into an open local-HTTP proxyconvex/contactMessages.submithad no length bounds, no email validation, and no throttle — cheap DoS / storage-bill vectorChanges
package.json— addedprotobufjs: ^7.5.5to the existingoverridesblock; bumpeddompurify^3.1.7→^3.4.0. Lockfile updated.src-tauri/sidecar/local-api-server.mjs— default-deny whenLOCAL_API_TOKENis unset/empty (503 + log). Tauri Rust shell already sets the token at launch, so production behavior is unchanged; this only closes the standalone/Docker hole.convex/contactMessages.ts— RFC-aligned per-field length caps, email shape check, control-character stripping, and a per-normalized-email rate limit (5 submissions / 1h) backed by a newby_normalized_email_receivedindex. Edge-side validation inserver/worldmonitor/leads/v1/submit-contact.tsis unchanged; this is defence-in-depth against direct Convex-client calls bypassing the edge.convex/schema.ts— addednormalizedEmailfield + index oncontactMessages.Tests
convex/__tests__/contactMessages.test.ts(7 cases): valid submission, malformed-email rejection, empty-name rejection, oversized-field clipping, control-char stripping, rate-limit threshold, and case-insensitive email normalization for the rate-limit bucket.src-tauri/sidecar/local-api-server.test.mjs.authFetchhelper now that the global gate is active.Verification
npm run typecheckandnpm run typecheck:apicleannpm run test:convex— 90/90 passnpm run test:sidecar— all targets pass; the only remaining failure (strips browser origin headers when proxying to cloud fallback) is a pre-existing flake onmain, unrelated to this PRnpm auditconfirms the protobufjs critical and dompurify moderate advisories are resolved