fix(backend): add Zod validation, helmet, and rate limiting across all routes#47
Open
stephanieoghenemega-eng wants to merge 20 commits into
Conversation
…tize Supabase errors
…q errors from 500 responses
…routes; sanitize errors
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the backend hardening issue — unvalidated request bodies, no security headers, no rate limiting, and raw service errors leaking to clients.
Commit-by-commit breakdown
Commit 1 —
chore(backend): install helmet, express-rate-limit, zod, vitest, supertestAdded to
backend/package.json:"dependencies": { + "express-rate-limit": "^7.5.0", + "helmet": "^8.0.0", + "zod": "^3.24.0" }, "devDependencies": { + "@types/supertest": "^6.0.2", + "supertest": "^7.0.0", + "vitest": "^2.1.0" }, "scripts": { + "test": "vitest run", + "test:watch": "vitest" }Commit 2 —
feat(backend): add helmet security-headers middlewarehelmet()is now the first middleware applied inindex.ts. It setsX-Content-Type-Options,X-Frame-Options,Strict-Transport-Security,Content-Security-Policy, and a dozen others automatically.Commit 3 —
feat(backend): add global rate limiter — 100 req / 15 min per IPNew file
backend/src/middleware/rateLimiter.ts:Wired in
index.tsbefore all routes:Commit 4 —
feat(backend): add validateBody/validateQuery Zod middleware helpersNew file
backend/src/middleware/validate.ts:Before this, every route had its own ad-hoc checks like:
After, every invalid request returns a consistent structured body:
{ "error": "Validation failed", "details": [ { "field": "content", "message": "content is required" }, { "field": "sender_address", "message": "must be a valid Stellar G-address" } ] }Commit 5 —
feat(backend): add shared internalError/serviceUnavailable response helpersNew file
backend/src/lib/errors.ts:Before, Supabase error messages were sent directly to the client:
After:
Commit 6 —
feat(backend/schemas): add Zod schemas for all notifications routesNew file
backend/src/schemas/notifications.ts:The old code only checked
wallet.startsWith("G")— a 2-character string like"GA"would pass. The new regex enforces the full 56-character Stellar G-address format.Commit 7 —
feat(backend/notifications): apply Zod validation to GET /notificationsAlso applied
validateQuery/validateBody+internalError/serviceUnavailabletoPATCH /:id/readandPOST /in the same pass, covering all three notification endpoints.Commit 8 —
feat(backend/schemas): add Zod schemas for all messages routesNew file
backend/src/schemas/messages.ts— one schema per endpoint:Note: the
sinceparam now rejects arbitrary strings — it must be a valid ISO 8601 datetime, preventing garbage values from being forwarded to Supabase.Commit 9 —
feat(backend/messages): replace manual checks with Zod validation on POST /Also removed the redundant
.slice(0, 4000)— Zod'smax(4000)already rejects anything longer before it reaches the DB.Commit 10 —
feat(backend/messages): apply Zod validation to GET /conversationThe
sinceparam is also now validated as a datetime before being passed to.gt("created_at", since).Commit 11 —
feat(backend/messages): apply Zod validation to GET /inboxCommit 12 —
feat(backend/messages): apply Zod validation to GET /unread-countCommit 13 —
feat(backend/messages): apply Zod validation to PATCH /conversation/readCommit 14 —
feat(backend/messages): apply Zod validation to PATCH /:id/read; sanitize Supabase errorsAt this point all 6 message endpoints are fully covered and no raw STELLAR_ADDR/UUID_RE regex or raw
error.messageleaks remain inmessages.ts.Commit 15 —
feat(backend/schemas): add Zod schemas for all AI route bodiesNew file
backend/src/schemas/ai.ts:Schemas also enforce maximum lengths on all string fields — previously the AI routes passed unbounded user input directly to the Groq API.
Commit 16 —
feat(backend/ai): apply Zod validation to POST /milestonesCommit 17 —
feat(backend/ai): apply Zod validation to POST /cover-letterCommit 18 —
feat(backend/ai): apply Zod validation to POST /rewrite; sanitize Groq errors from 500 responsesSame sanitization applied to
/milestonesand/cover-letterin their respective commits.Commit 19 —
feat(backend): add Zod schemas and validation for gasless and upload routes; sanitize errorsbackend/src/schemas/gasless.ts:backend/src/schemas/upload.ts:Applied in their routes:
Commit 20 —
test(backend): add vitest validation tests for all route schemasNew file
backend/src/__tests__/validation.test.ts— 30 unit tests that validate the Zod schemas directly, covering every route:Run with:
Acceptance criteria met
400with structured{ error, details }bodyRateLimit-*headers)helmet()applied to all responses