Skip to main content

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.
Our security design follows three principles:
  1. Defense in Depth: Multiple layers of validation. Even if one layer is bypassed, others catch the attack.
  2. Least Privilege: Components only have access to what they need. Users default to minimal permissions.
  3. 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():
// src/lib/security.ts
export function getSafeRedirectPath(path: string | null): string {
  // Block protocol-relative URLs (//evil.com)
  if (path?.startsWith("//")) return "/";
  
  // Block absolute URLs (https://evil.com)
  if (path?.includes("://")) return "/";
  
  // Block dangerous protocols (javascript:, data:)
  if (DANGEROUS_PROTOCOLS.some(p => path?.toLowerCase().startsWith(p))) {
    return "/";
  }
  
  // Only allow relative paths starting with /
  if (!path?.startsWith("/")) return "/";
  
  return path;
}
Applied In:
  • 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: All dangerouslySetInnerHTML content is sanitized with DOMPurify:
import DOMPurify from 'isomorphic-dompurify';

// Sanitize before rendering
<div 
  dangerouslySetInnerHTML={{ 
    __html: DOMPurify.sanitize(markdownContent) 
  }} 
/>
Applied In:
  • 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

// src/components/engine/fields/files-field.tsx
function cleanFileName(filename: string): string {
  let name = filename
    .replace(/\.\./g, "")   // Remove ..
    .replace(/\\/g, "")     // Remove backslashes
    .replace(/\//g, "");    // Remove forward slashes
  
  // Replace special chars with dashes
  name = name.replace(/[^\w\-]/g, "-");
  
  return name + cleanExtension;
}

Layer 2: Server-Side Validation

// src/lib/security.ts
export function sanitizeStoragePath(path: string): string {
  // Block path traversal
  if (path.includes("..")) {
    throw new Error("Invalid file path: path traversal not allowed");
  }
  
  // Block backslashes
  if (path.includes("\\")) {
    throw new Error("Invalid file path: backslashes not allowed");
  }
  
  // Block absolute paths
  if (path.startsWith("/")) {
    throw new Error("Invalid file path: absolute paths not allowed");
  }
  
  // Validate against safe pattern
  if (!/^[\w\-./]+$/.test(path)) {
    return cleanedPath; // Auto-sanitize unsafe characters
  }
  
  return path;
}

Layer 3: Safe Path Construction

// src/lib/security.ts
export function buildSafeStoragePath(
  formId: string,
  fileId: string,
  fileName: string
): string {
  // Sanitize each component individually
  const safeFormId = formId.replace(/[^\w\-]/g, "");
  const safeFileId = fileId.replace(/[^\w\-]/g, "");
  const safeFileName = cleanFileName(fileName);
  
  const path = `${safeFormId}/${safeFileId}/${safeFileName}`;
  
  // Final validation
  return sanitizeStoragePath(path);
}
Applied In:
  • File upload (src/app/actions.tsuploadFormFile)
  • File deletion (src/app/actions.tsdeleteFormFile)
  • Signed URL generation (src/app/actions.tsgetSecureFileUrl)
  • 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 applicant on error)
  • Explicit admin role check (super_admin or admin)
  • Middleware blocks all admin routes for non-admin users

Security Utilities

All security functions live in src/lib/security.ts:
FunctionPurpose
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

Attack: https://app.example.com/login?next=https://evil.comDefense:
  1. getSafeRedirectPath detects :// in the path
  2. Returns / instead of the malicious URL
  3. User is redirected to dashboard, not evil.com
Attack: Upload file named ../../../etc/passwdDefense:
  1. Client-side cleanFileName strips ..etcpasswd
  2. Server-side buildSafeStoragePath validates each component
  3. sanitizeStoragePath blocks any remaining traversal patterns
  4. File is stored safely as form-123/uuid-456/etcpasswd
Attack: Form description contains <script>alert('xss')</script>Defense:
  1. DOMPurify.sanitize() strips all script tags
  2. Only safe HTML elements remain
  3. Formatting (bold, links) still works
Attack: File path form-123/%2e%2e/%2e%2e/etc/passwdDefense:
  1. sanitizeStoragePath decodes the path
  2. Detects .. in decoded form
  3. Throws error, blocking the operation
Attack: ?next=javascript:alert(document.cookie)Defense:
  1. getSafeRedirectPath checks against DANGEROUS_PROTOCOLS
  2. javascript: is in the blocklist
  3. 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:
  1. Understand the risk: Is this a true positive or false positive?
  2. Apply the appropriate fix: Use the security utilities documented above
  3. Make fixes blatant: Scanners can’t reason about control flow—make safety obvious in the code
  4. Document edge cases: If a pattern is intentionally different, add a comment explaining why

Dependencies

PackageVersionPurpose
isomorphic-dompurify^2.xXSS sanitization (SSR-safe)
zod^4.xInput validation schemas

Next: Authentication & RBAC

Learn about user roles and access control.