Closed
🔒 Fix critical security gaps: OCR rate limiting, SSRF validation, CSP, input sanitization#1
Conversation
Copilot
AI
changed the title
🔒 Critical Security Fixes: OCR Rate Limiting, SSRF Validation, CSP Headers, Input Sanitization
🔒 Fix critical security gaps: OCR rate limiting, SSRF validation, CSP, input sanitization
May 22, 2026
Copilot created this pull request from a session on behalf of
Shinitaii
May 22, 2026 15:06
View session
There was a problem hiding this comment.
Pull request overview
This PR introduces defense-in-depth security hardening across the API OCR endpoints and clients, including new OCR-specific rate limiting, SSRF-focused URL validation, CSP/HSTS headers, and global request input sanitization, plus a rewritten mobile API client with timeouts and structured errors.
Changes:
- Add an OCR-specific rate limiter (30 req/hr) and apply it to
/billing-cycles/ocrand/bills/ocr. - Add URL validation and error handling around OCR image URL processing; add global request input sanitization middleware.
- Add Helmet CSP/HSTS configuration and harden the mobile API client with timeouts + typed errors.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| SECURITY_FIXES_SUMMARY.md | Adds a security implementation summary document describing the mitigations and rollout/testing notes. |
| api/functions/src/config/rate-limit.config.ts | Introduces ocrRateLimiter alongside existing auth/api rate limiters. |
| api/functions/src/features/billing-cycle/billing-cycle.controller.ts | Adds OCR URL validation + logging/error handling around Gemini bill extraction. |
| api/functions/src/features/billing-cycle/billing-cycle.route.ts | Applies ocrRateLimiter to the billing-cycle OCR route. |
| api/functions/src/features/bills/bills.controller.ts | Adds OCR URL validation + logging/error handling around Gemini bill extraction. |
| api/functions/src/features/bills/bills.route.ts | Applies ocrRateLimiter to the bills OCR route. |
| api/functions/src/index.ts | Adds CSP/HSTS via Helmet and installs the global input sanitization middleware. |
| api/functions/src/middlewares/sanitize-input.middleware.ts | New middleware to recursively strip HTML tags from request body/query/params. |
| mobile/src/lib/api/client.ts | Rewrites mobile API client to add timeouts, structured errors, and centralized response handling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+56
to
+60
| frameSrc: ["'none'"], | ||
| objectSrc: ["'none'"], | ||
| upgradeInsecureRequests: [], | ||
| }, | ||
| }, |
Comment on lines
+16
to
+22
| * Recursively sanitizes all string values in an object. | ||
| * Removes HTML tags and dangerous characters while preserving structure. | ||
| */ | ||
| function sanitizeObject(obj: any): any { | ||
| if (typeof obj === 'string') { | ||
| // Remove HTML tags and escape dangerous characters | ||
| return stripHtml(obj).result.trim(); |
Comment on lines
+57
to
+61
| // Block RFC-1918 (private networks), loopback, link-local, and metadata services | ||
| const blockedPattern = | ||
| /^(10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|127\.\d+\.\d+\.\d+|169\.254\.\d+\.\d+|::1|localhost|metadata\.google\.internal|169\.254\.169\.254)/i; | ||
|
|
||
| if (blockedPattern.test(parsed.hostname)) { |
Comment on lines
+31
to
+33
| if (error instanceof Error && error.message.includes('Invalid image URL')) { | ||
| throw new AppError(400, error.message); | ||
| } |
Comment on lines
+167
to
+171
| const blockedPattern = | ||
| /^(10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|127\.\d+\.\d+\.\d+|169\.254\.\d+\.\d+|::1|localhost|metadata\.google\.internal|169\.254\.169\.254)/i; | ||
|
|
||
| if (blockedPattern.test(parsed.hostname)) { | ||
| throw new AppError(400, 'Invalid image URL: private or reserved IP addresses are not allowed'); |
Comment on lines
+120
to
+129
| export async function apiGet<T = any>(endpoint: string): Promise<T> { | ||
| try { | ||
| const res = await request(endpoint, { method: 'GET' }); | ||
| return handleResponse<T>(res); | ||
| } catch (error) { | ||
| if (error instanceof ApiError || error instanceof NetworkError) { | ||
| throw error; | ||
| } | ||
| throw new NetworkError(`Failed to fetch ${endpoint}`); | ||
| } |
Comment on lines
+43
to
+46
| keyGenerator: (req) => { | ||
| // Use user ID if authenticated, fallback to IP | ||
| return (req as any).user?.userId || req.ip || 'unknown'; | ||
| }, |
| - Attack metadata services (e.g., GCP: `metadata.google.internal`) | ||
|
|
||
| ### Solution | ||
| Implemented client-side SSRF validation that validates URLs before passing to Gemini API. |
Comment on lines
+39
to
+47
| /** | ||
| * Validates that a URL is safe for OCR processing. | ||
| * Prevents SSRF attacks by blocking private/local addresses. | ||
| */ | ||
| function validateOcrUrl(url: string): void { | ||
| // Block data: URLs (client should send actual URLs) | ||
| if (url.startsWith('data:')) { | ||
| throw new AppError(400, 'Data URLs are not supported for this endpoint'); | ||
| } |
Comment on lines
+148
to
+156
| /** | ||
| * Validates that a URL is safe for OCR processing. | ||
| * Prevents SSRF attacks by blocking private/local addresses. | ||
| */ | ||
| function validateOcrUrl(url: string): void { | ||
| // Block data: URLs (already handled by Gemini lib, but double-check) | ||
| if (url.startsWith('data:')) { | ||
| throw new AppError(400, 'Data URLs are not supported for this endpoint'); | ||
| } |
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.
Code review audit identified several critical security vulnerabilities: unprotected resource-intensive OCR endpoints vulnerable to abuse, SSRF attack surface on image URL processing, missing content security headers, and inconsistent input sanitization. This PR addresses all five with defense-in-depth mitigations.
Changes
OCR Rate Limiting
ocrRateLimiterrestricting to 30 requests/hour per authenticated user (vs unlimited)POST /billing-cycles/ocrandPOST /bills/ocrSSRF Validation
Content Security Policy Headers
Global Input Sanitization Middleware
Mobile API Client Hardening
ApiError/NetworkErrorclasses replacing silent failuresTesting
Rate limit test: 30 requests succeed, 31st returns 429. SSRF test: private IP ranges rejected with clear error. CSP headers present in response. Mobile timeouts after 15s on slow connections.
Deployment Notes
VITE_API_BASE_URL)