HR Insight handles sensitive business data (employee salaries, financial records, corporate strategy). This document covers all security layers, from authentication to data validation, CSP headers, SRI hashes, and App Check.
- Users authenticate via Google OAuth 2.0 through Firebase Authentication
- No passwords stored — Firebase manages token lifecycle
- ID tokens auto-refresh every 55 minutes
onAuthStateChangedlistener keeps session state reactive
- Frontend: Firebase SDK handles token refresh transparently
- Cloud Functions:
onCalltrigger auto-verifies Firebase ID token — no manual verification needed - Firestore: Security rules check
request.auth != nullon every operation
initializeAppCheckwithReCaptchaV3Providerinitialized insrc/firebase.ts- Every request to Firebase services includes an App Check token
- Protects against abuse from stolen Firebase config keys
- Requires
VITE_RECAPTCHA_SITE_KEYenv var (get from reCAPTCHA Admin)
Every collection enforces owner-based isolation:
allow read: if isAuthenticated() && resource.data.uid == request.auth.uid;
allow create: if isAuthenticated() && isOwner(request.resource.data.uid);
allow update: if isAuthenticated() && isOwner(resource.data.uid) && uidNotModified();
Guarantees:
- User A cannot read, write, or infer data from User B
uidfield cannot be changed by the client (uidNotModified())- Users can only create documents with their own
uid
allow update: if isAuthenticated()
&& !('plan' in request.resource.data)
&& !('status' in request.resource.data)
&& !('currentPeriodEnd' in request.resource.data);
allow delete: if false;
Plan upgrades can ONLY happen through the upgradeToPro Cloud Function (Admin SDK, server-side trusted context).
All user input is sanitized before being sent to Firestore:
sanitizeInput(input, maxLength) — removes XSS vectors from plain text:
<script>tags and contentjavascript:URIsvbscript:URIsdata:text/htmlURIs- Event handlers:
onerror=,onclick=,onload=,onmouseover=, etc. expression()(CSS IE hack)<iframe>,<object>,<embed>,<form>tags
Applied to ALL form fields in useFirestoreMutations:
- Employee: name, position, section, area
- Expense: description
- Inventory: name, category
- Revenue: category, description
- Portfolio: companyName, tagline, description, logoUrl, mission, vision, values
sanitizeMarkdown(html) — DOMPurify wrapper for AI output:
- Whitelist:
h1-h6,p,strong,em,b,i,ul,ol,li,code,pre,blockquote,a,br,hr,table,thead,tbody,tr,th,td - Allowed attrs:
href,title,class,target,rel - Forbidden:
onclick,onerror,onload,onmouseover,style,script,iframe,object,embed,form,img - Used in
AiAnalysisModal.tsxandAiTab.tsx
isValidImageUrl(url) — validates image URLs:
- Must be
https://from known safe domains (googleusercontent.com, firebaseapp.com, github.com, etc.) - Used for
user.photoURLin Sidebar
Each collection has a validation function that runs on every write:
| Collection | Validated Fields |
|---|---|
employees |
name (1-99 chars), role (enum), salary (>=0), performance (1-10), status (enum), etc. |
expenses |
type (enum), amount (>=0), currency (enum), description (<500 chars) |
inventory |
name (1-199 chars), quantity (>=0), unitPrice (>=0) |
revenue |
type (in/out), amount (>=0), category (1-99 chars), description (<500 chars) |
portfolios |
companyName (1-99 chars), optional fields with size limits |
src/utils/importExport.ts validates every record before writing to Firestore:
- Type checks for all fields
- String length limits
- Enum validation
- Numeric range validation
- Max 500 records per import (DoS prevention)
functions/src/index.ts — three layers of validation:
- Rate Limiting: 10 requests/minute per uid, in-memory Map with 5-min cleanup
- Input Sanitization: Removes prompt injection patterns (
IGNORE,DISREGARD,SYSTEM PROMPT,OVERRIDE,PREVIOUS INSTRUCTION) - Payload Validation: Type guards (
isValidAnalyzePayload,isValidStrategicPayload) check every field type and range
| Layer | Mechanism | What it blocks |
|---|---|---|
| 1 | React auto-escape | All JSX content is escaped by default |
| 2 | Frontend sanitizeInput |
Script tags, event handlers, javascript: URIs in form inputs |
| 3 | DOMPurify on AI output | Strips dangerous tags/attrs from markdown rendering |
| 4 | Firestore rules validation | Rejects writes with invalid data types/sizes |
| 5 | CSP headers | Blocks inline scripts, unauthorized sources |
| 6 | SRI hashes | Ensures loaded scripts haven't been tampered with |
| 7 | Cloud Functions sanitization | Server-side input cleaning before AI processing |
No dangerouslySetInnerHTML used anywhere in the codebase.
Configured in firebase.json hosting headers:
| Header | Value | Purpose |
|---|---|---|
Content-Security-Policy |
default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://*.googleapis.com ...; frame-ancestors 'none'; base-uri 'self'; form-action 'self' |
Blocks inline scripts, unauthorized sources, clickjacking |
X-Content-Type-Options |
nosniff |
Prevents MIME type sniffing |
X-Frame-Options |
DENY |
Prevents clickjacking via iframes |
Referrer-Policy |
strict-origin-when-cross-origin |
Limits referrer info |
Permissions-Policy |
camera=(), microphone=(), geolocation=() |
Disables browser APIs |
All <script> and <link> tags in the production index.html include integrity="sha384-..." and crossorigin="anonymous" attributes.
Generated by a custom Vite plugin (sriHashPlugin in vite.config.ts) that:
- Hashes each JS/CSS chunk with SHA-384 during build
- Injects integrity attributes into the HTML output
- Ensures tampered scripts are rejected by the browser
CSRF is mitigated by the architecture:
- Firebase Auth uses Bearer tokens, not cookies — no CSRF token needed
- Cloud Functions
onCalluse Firebase ID tokens, not session cookies - Firestore SDK uses tokens, not sessions
- No server-side sessions that could be exploited via CSRF
The AI proxy uses multiple defenses:
"Você é um consultor de RH sênior. Analise o desempenho do funcionário...
Responda apenas com a análise profissional."
System instructions are set server-side and cannot be overridden by user input.
function sanitizeInput(value: string, maxLength: number): string {
return value
.slice(0, maxLength)
.replace(/(?:IGNORE|DISREGARD|NEW\s*INSTRUCTION|SYSTEM\s*PROMPT|OVERRIDE|PREVIOUS\s*INSTRUCTION)/gi, "")
.trim();
}All string inputs are truncated to their maximum allowed length, preventing oversized payloads.
src/components/ErrorBoundary.tsx catches React render errors and displays a generic fallback ("Ops! Algo deu errado") — no error details exposed to users. Details logged to console only.
src/utils/currency.ts — handleFirestoreError() logs operation type, collection path, and auth info (without PII) for debugging.
- Auth failures →
unauthenticated - Rate limit →
resource-exhausted - Invalid input →
invalid-argument - All errors use Firebase's standard
HttpsErrortypes
- Firebase config values (project ID, API key, app ID) — these are public by design
VITE_RECAPTCHA_SITE_KEY— reCAPTCHA v3 site key for App CheckVITE_GEMINI_API_KEY— DEPRECATED, no longer used
GEMINI_API_KEY— set viafirebase functions:config:set gemini.api_key, never committed to repo
The vite.config.ts no longer bakes GEMINI_API_KEY into the client bundle. All AI calls go through Cloud Functions with server-side key.
The file .env.local contains real Firebase credentials and is committed to the repository. This should be:
- Added to
.gitignore - Credentials rotated
- New credentials set via environment variables
| Control | Status | Notes |
|---|---|---|
| Authentication required for all operations | ✅ | Firebase Auth + Security Rules |
| Multi-tenant data isolation | ✅ | uid check on every collection |
| Firebase App Check | ✅ | reCAPTCHA v3 |
| Input sanitization (frontend) | ✅ | sanitizeInput removes XSS vectors |
| Input sanitization (server-side) | ✅ | Cloud Functions sanitize before AI |
| DOMPurify on AI output | ✅ | Whitelist-only, no scripts/iframes |
| Content Security Policy | ✅ | CSP + X-Frame-Options + nosniff |
| Subresource Integrity | ✅ | SHA-384 hashes on all scripts/styles |
| Rate limiting on AI endpoint | ✅ | 10 req/min per uid |
| Prompt injection prevention | ✅ | System instruction + sanitization |
| XSS prevention (7 layers) | ✅ | React escape → sanitize → DOMPurify → rules → CSP → SRI → CF |
| CSRF mitigation | ✅ | Token-based auth (no cookies) |
| Subscription field protection | ✅ | Client cannot modify plan/status |
| Error boundaries (no info leak) | ✅ | Generic message to users |
| Photo URL validation | ✅ | Only known safe domains |
| Secrets in env vars | .env.local exposed — needs rotation |
|
| HTTPS only | ✅ | Firebase enforces HTTPS |
| CORS configured | ✅ | Cloud Functions handle CORS |
- Rotate Firebase API key in Google Cloud Console
- Rotate Gemini API key
- Update
.env.localand redeploy - Add
.env.localto.gitignore
- User sees: "Rate limit exceeded. Try again later."
- Cloud Function returns
resource-exhaustederror - Frontend displays toast notification
- Check browser console for
permission-deniederror - Verify
uidmatches authenticated user - Verify all required fields pass validation functions
- Frontend
sanitizeInputstrips the payload silently - CSP blocks inline script execution
- SRI prevents tampered script loading
- Log the attempt for investigation