Skip to content

fix(security): harden sidecar auth, bump deps, validate contactMessages#3388

Open
svanvliet wants to merge 1 commit into
koala73:mainfrom
svanvliet:fix/security-hardening
Open

fix(security): harden sidecar auth, bump deps, validate contactMessages#3388
svanvliet wants to merge 1 commit into
koala73:mainfrom
svanvliet:fix/security-hardening

Conversation

@svanvliet
Copy link
Copy Markdown

Addresses one Critical, two High, and one Medium item from the recent internal security review.

Fixes

ID Severity Item
C2 Critical protobufjs RCE (GHSA-xq3m-2v4x-88gg, CVSS 9.8) reachable through @xenova/transformersonnx-proto
H1 High Sidecar LOCAL_API_TOKEN fail-open: unset token silently disabled the auth gate, turning any standalone sidecar into an open local-HTTP proxy
H2 High DOMPurify advisories (GHSA-39q2-94rc-95cp, GHSA-h7mw-gpvr-xq4m) on the LLM-widget sanitizer
M2 Medium convex/contactMessages.submit had no length bounds, no email validation, and no throttle — cheap DoS / storage-bill vector

Changes

  • package.json — added protobufjs: ^7.5.5 to the existing overrides block; bumped dompurify ^3.1.7^3.4.0. Lockfile updated.
  • src-tauri/sidecar/local-api-server.mjs — default-deny when LOCAL_API_TOKEN is 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 new by_normalized_email_received index. Edge-side validation in server/worldmonitor/leads/v1/submit-contact.ts is unchanged; this is defence-in-depth against direct Convex-client calls bypassing the edge.
  • convex/schema.ts — added normalizedEmail field + index on contactMessages.

Tests

  • New 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.
  • New default-deny regression test in src-tauri/sidecar/local-api-server.test.mjs.
  • Existing sidecar tests updated to use a shared authFetch helper now that the global gate is active.

Verification

  • npm run typecheck and npm run typecheck:api clean
  • npm run test:convex — 90/90 pass
  • npm run test:sidecar — all targets pass; the only remaining failure (strips browser origin headers when proxying to cloud fallback) is a pre-existing flake on main, unrelated to this PR
  • npm audit confirms the protobufjs critical and dompurify moderate advisories are resolved
  • ✅ Biome lint clean on all changed files

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>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 24, 2026

@svanvliet is attempting to deploy a commit to the World Monitor Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the trust:caution Brin: contributor trust score caution label Apr 24, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 24, 2026

Greptile Summary

This PR closes four security findings from an internal audit: a protobufjs RCE reachable through @xenova/transformers (GHSA-xq3m-2v4x-88gg, CVSS 9.8), a sidecar auth fail-open that let any standalone instance become an open local HTTP proxy, two DOMPurify advisories, and a convex/contactMessages mutation with no validation or throttle. The dependency bumps and schema additions are straightforward; the auth hardening and Convex mutation hardening are well-structured and clearly explained.

Confidence Score: 4/5

Safe 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 normalizedEmail won't participate in rate-limit counting. Neither affects runtime correctness.

convex/schema.ts — the optional normalizedEmail field means pre-existing rows silently bypass the rate-limit window after deployment.

Important Files Changed

Filename Overview
convex/contactMessages.ts Adds field-length caps, control-char stripping, email validation, and per-email rate limiting; logic is sound with one minor concern about pre-existing rows bypassing the rate-limit window.
convex/schema.ts Adds normalizedEmail as an optional field and a compound index for rate-limiting; optional declaration means pre-migration rows won't participate in rate-limit queries.
src-tauri/sidecar/local-api-server.mjs Default-deny auth fix is clean: empty/unset LOCAL_API_TOKEN now returns 503 instead of silently disabling the gate; pre-auth health-check exemption preserved.
src-tauri/sidecar/local-api-server.test.mjs Adds correct default-deny regression test and updates all existing tests to use authFetch; one test comment misleadingly labels a wrong-token request as "without auth header".
convex/tests/contactMessages.test.ts Seven-case test suite covering valid submission, email rejection, name rejection, field clipping, control-char stripping, rate-limit threshold, and case-insensitive rate-limit bucketing; thorough and accurate.
package.json Bumps dompurify to ^3.4.0 and adds protobufjs: ^7.5.5 to the overrides block to address GHSA advisories; straightforward dep changes.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (1): Last reviewed commit: "fix(security): harden sidecar auth, bump..." | Re-trigger Greptile

Comment thread convex/schema.ts
Comment on lines +179 to +180
normalizedEmail: v.optional(v.string()),
}).index("by_normalized_email_received", ["normalizedEmail", "receivedAt"]),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment on lines 1025 to +1026
// 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`, {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

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

Labels

trust:caution Brin: contributor trust score caution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant