Security
Terra is built with a defense-in-depth security model. Every layer—from client to database—includes protections against common vulnerabilities. This guide documents our approach and implementations.Security Philosophy
Fail Secure: When in doubt, deny access. If a security check fails or throws an error, the default is to block the action, not allow it.
- Defense in Depth: Multiple layers of validation. Even if one layer is bypassed, others catch the attack.
- Least Privilege: Components only have access to what they need. Users default to minimal permissions.
- Input Distrust: All user input is treated as potentially malicious. Sanitization happens at every boundary.
Vulnerability Protections
Open Redirect Prevention
Risk: An attacker crafts a URL like/login?next=https://evil.com to steal credentials or phish users.
Protection: All redirect paths are validated through getSafeRedirectPath():
- Middleware redirects (
src/middleware.ts) - OAuth callback redirects (
src/app/auth/callback/route.ts) - Login deep linking (
src/app/login/page.tsx)
XSS (Cross-Site Scripting) Prevention
Risk: User-generated content (like form labels or markdown descriptions) could contain malicious scripts. Protection: AlldangerouslySetInnerHTML content is sanitized with DOMPurify:
- Public program landing pages (
src/app/p/[slug]/client.tsx) - Public form viewer (
src/app/f/[slug]/client.tsx) - Info fields in forms (
src/components/engine/fields/info-field.tsx) - Public intake page (
src/app/(public)/intake/page.tsx)
We use
isomorphic-dompurify instead of plain dompurify because it works in both server and client environments (SSR-safe).Path Traversal Prevention
Risk: A malicious user uploads a file named../../etc/passwd or ..\..\secrets.txt to access files outside the intended storage directory.
Protection: All file paths are sanitized at multiple layers:
Layer 1: Client-Side Sanitization
Layer 2: Server-Side Validation
Layer 3: Safe Path Construction
- File upload (
src/app/actions.ts→uploadFormFile) - File deletion (
src/app/actions.ts→deleteFormFile) - Signed URL generation (
src/app/actions.ts→getSecureFileUrl) - Client upload component (
src/components/engine/fields/files-field.tsx)
RBAC (Role-Based Access Control)
See the Authentication & RBAC Guide for complete documentation. Key Points:- Fail-secure role lookup (default to
applicanton error) - Explicit admin role check (
super_adminoradmin) - Middleware blocks all admin routes for non-admin users
Security Utilities
All security functions live insrc/lib/security.ts:
| Function | Purpose |
|---|---|
getSafeRedirectPath(path) | Validates redirect paths, blocks open redirects |
isValidExternalUrl(url) | Validates admin-configured external URLs |
createSafeInternalRedirect(path, baseUrl) | Creates same-origin redirect URLs |
cleanFileName(filename) | Sanitizes filenames for storage |
sanitizeStoragePath(path) | Validates full storage paths |
buildSafeStoragePath(formId, fileId, fileName) | Constructs safe storage paths |
Attack Scenarios & Defenses
Attacker tries open redirect via login
Attacker tries open redirect via login
Attack:
https://app.example.com/login?next=https://evil.comDefense:getSafeRedirectPathdetects://in the path- Returns
/instead of the malicious URL - User is redirected to dashboard, not evil.com
Attacker uploads malicious filename
Attacker uploads malicious filename
Attack: Upload file named
../../../etc/passwdDefense:- Client-side
cleanFileNamestrips..→etcpasswd - Server-side
buildSafeStoragePathvalidates each component sanitizeStoragePathblocks any remaining traversal patterns- File is stored safely as
form-123/uuid-456/etcpasswd
Attacker injects script in form description
Attacker injects script in form description
Attack: Form description contains
<script>alert('xss')</script>Defense:DOMPurify.sanitize()strips all script tags- Only safe HTML elements remain
- Formatting (bold, links) still works
Attacker tries encoded path traversal
Attacker tries encoded path traversal
Attack: File path
form-123/%2e%2e/%2e%2e/etc/passwdDefense:sanitizeStoragePathdecodes the path- Detects
..in decoded form - Throws error, blocking the operation
Attacker tries javascript: protocol redirect
Attacker tries javascript: protocol redirect
Attack:
?next=javascript:alert(document.cookie)Defense:getSafeRedirectPathchecks againstDANGEROUS_PROTOCOLSjavascript:is in the blocklist- Returns
/instead
Security Checklist
When adding new features, verify:1
User Input
Is all user input validated and sanitized?
2
Redirects
Do all redirects use
getSafeRedirectPath or createSafeInternalRedirect?3
HTML Rendering
Is
dangerouslySetInnerHTML wrapped with DOMPurify.sanitize()?4
File Paths
Do file operations use
sanitizeStoragePath or buildSafeStoragePath?5
Authorization
Is access control checked at the start of server actions?
6
Error Handling
Do errors fail secure (deny access) rather than fail open?
Security Scanning
Terra uses Aikido for automated security scanning:- SAST: Static analysis on every PR
- Dependency Scanning: Alerts for vulnerable packages
- Secret Detection: Blocks commits with exposed credentials
Addressing Flags
When Aikido flags a potential vulnerability:- Understand the risk: Is this a true positive or false positive?
- Apply the appropriate fix: Use the security utilities documented above
- Make fixes blatant: Scanners can’t reason about control flow—make safety obvious in the code
- Document edge cases: If a pattern is intentionally different, add a comment explaining why
Dependencies
| Package | Version | Purpose |
|---|---|---|
isomorphic-dompurify | ^2.x | XSS sanitization (SSR-safe) |
zod | ^4.x | Input validation schemas |
Next: Authentication & RBAC
Learn about user roles and access control.