BF Fund Investor Dataroom is designed with security as a core principle. This document outlines implemented security controls, known limitations, and vulnerability reporting procedures.
| Version | Supported |
|---|---|
| 1.x.x | ✅ |
If you discover a security vulnerability, please report it responsibly:
- Do NOT create a public GitHub issue
- Email security concerns to: [email protected]
- Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact assessment
- Any suggested remediation
We aim to respond within 48 hours and will work with you to understand and address the issue.
Before deploying, ensure:
- TLS Termination: Deploy behind a reverse proxy or edge provider that enforces HTTPS (e.g., Vercel, Cloudflare, AWS ALB)
- Environment Variables: Configure all required secrets via secure secret management
- TLS: Requires deployment behind TLS-terminating proxy (application does not handle TLS directly)
- HSTS: Strict-Transport-Security header set in production responses - see
lib/middleware/csp.ts:153-157
Implemented:
- NextAuth.js: Database-backed sessions with secure cookies
- Magic Links: Email-based authentication for LP users
- Google OAuth: Admin/GP authentication
- Session Security (verified in
lib/auth/auth-options.ts):- HttpOnly cookies
- Secure flag (HTTPS only)
SameSite=Nonefor cross-site/iframe compatibility (updated February 2026)
Role-Based Access Control:
- LP (Limited Partner): Access to personal documents, subscriptions, portal
- GP (General Partner): Fund management, investor oversight
- Admin: Full platform access, user management
- Viewer: Read-only document access
SSO Configuration (February 2026):
- Supported providers: SAML 2.0, OIDC, Azure AD, Okta, Google Workspace
- Organization-level SSO configuration stored in
Organization.ssoConfig - Sensitive SSO credentials encrypted via
OrganizationIntegrationCredential - Certificate/private key storage with AES-256-GCM encryption
Organization Security Policies:
OrganizationSecurityPolicymodel enforces tenant-wide security:- MFA requirements (
requireMfa) - Session timeout configuration
- IP allowlist/denylist
- Allowed authentication providers
- Document watermarking requirements
- Lockable policy keys (prevent override at fund/object level)
- MFA requirements (
Defaults Inheritance Hierarchy:
- Organization Defaults → Team Defaults → Fund/Dataroom/Link Settings
OrganizationDefaultsauto-applied to new teams and objectsTeamDefaultsoptionally override organization-level settings- Lockable keys prevent fund/object-level overrides for critical security settings
Implemented (ENABLED BY DEFAULT when key is configured):
- Server-Side Encryption: AES-256-GCM encryption in storage pipeline - see
lib/storage/encryption/crypto-service.ts - Encryption Key: Requires
STORAGE_ENCRYPTION_KEYenvironment variable (64-character hex string) - Default Behavior: When encryption key is set, ALL files are encrypted by default
- Use
{ encrypt: false }to explicitly store unencrypted (not recommended)
- Use
- Key Generation: Run
npm run generate:encryption-keyto create a secure key
Production Security:
- Password-derived keys are BLOCKED in production (
NODE_ENV=production) - Only proper 64-character hex keys (256-bit) are accepted in production
- Generate keys with:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
PDF-Level Encryption (Implemented):
- See
lib/crypto/pdf-encryption.tsfor AES-256 PDF 2.0 encryption usingpdf-lib-plus-encrypt - Supports user/owner passwords, permission controls, and watermarking
- Used for completed signature documents
Not Yet Implemented:
- Client-side encryption before upload
Integration Credential Encryption (February 2026):
OrganizationIntegrationCredentialmodel stores API keys and secrets- AES-256-GCM encryption using
STORAGE_ENCRYPTION_KEY - Credential service:
lib/organization/credential-service.ts - Per-credential integrity hash for tamper detection
- Automatic key rotation tracking via
lastRotatedAt
Implemented (see lib/middleware/csp.ts):
- Nonce-based script execution
- Strict domain whitelisting for scripts, connections, images
- Frame-ancestors restrictions (DENY by default)
- WASM support for PDF processing
Implemented (see lib/middleware/csp.ts):
| Header | Value | Status |
|---|---|---|
| Content-Security-Policy | Nonce-based | ✅ |
| X-Content-Type-Options | nosniff | ✅ |
| X-Frame-Options | DENY / SAMEORIGIN | ✅ |
| Referrer-Policy | strict-origin-when-cross-origin | ✅ |
| Permissions-Policy | camera=(), microphone=(), geolocation=() | ✅ |
| Strict-Transport-Security | max-age=31536000; includeSubDomains; preload | ✅ |
Implemented:
- sanitize-html (
lib/utils/sanitize-html.ts): Strips all HTML tags from user content- Used in: team name updates, branding settings, agreements, conversation features
- Path sanitization (
proxy.ts:53-58): Removes..sequences and normalizes slashes - Host validation (
proxy.ts:19-29): Validates host format and length - IP validation (
proxy.ts:31-47): Validates client IP format (IPv4/IPv6) - URL validation (
lib/notion/utils.ts): Domain allowlist for external URLs- Blocks path traversal sequences (
.., null bytes) - Enforces HTTPS protocol
- Only allows known Notion domains
- Blocks path traversal sequences (
- File path canonicalization (
lib/storage/providers/local-provider.ts):- URL decoding before validation
- Repeated
..sequence removal path.resolve()+ boundary checking- Throws error if path escapes storage directory
Schema (see prisma/schema.prisma lines 1298-1318):
AuditLogmodel fields: id, eventType, userId, teamId, resourceType, resourceId, ipAddress, userAgent, metadata, createdAt- Indexed on: teamId, userId, eventType, createdAt
Usage locations:
- Fund settings:
pages/api/funds/[fundId]/settings.ts - Transactions:
pages/api/transactions/index.ts - Data import/export:
pages/api/admin/import.ts,pages/api/admin/export.ts
Retention Policy (Implemented February 2026):
Audit log retention is managed via environment variables and a cron job at /api/cron/audit-retention:
| Log Type | Retention (Days) | Environment Variable |
|---|---|---|
| AuditLog | 2555 (7 years) | AUDIT_LOG_RETENTION_DAYS |
| SignatureAuditLog | 2555 (7 years) | SIGNATURE_AUDIT_LOG_RETENTION_DAYS |
- Default: 7 years (2555 days) - meets SEC/FINRA record retention requirements
- Check Status:
GET /api/cron/audit-retentionreturns current counts - Manual Cleanup:
POST /api/cron/audit-retentionruns the cleanup job
Limitations:
- Logs stored in standard PostgreSQL tables (not immutable append-only storage)
- For regulatory compliance, consider external append-only storage
As of this writing, npm audit reports 1 vulnerability:
Moderate (1) - ESLint 8.x Stack Overflow:
- Dependency chain:
eslint-config-next->[email protected] - Risk Acceptance: Development-only tool, not included in production builds
Status:
- All critical and high vulnerabilities have been resolved
- ESLint 8.x vulnerability accepted (dev-only, no production impact)
- Run
npm auditto verify current status before deployment
All file upload and storage operations are protected against path traversal attacks:
| File | Protection Method |
|---|---|
lib/storage/providers/local-provider.ts |
URL decoding, null byte removal, repeated .. removal, path.resolve() canonicalization, boundary checking |
pages/api/file/replit-upload.ts |
crypto.randomUUID() + character sanitization ([^a-zA-Z0-9.-] → _) |
pages/api/file/browser-upload.ts |
Vercel Blob with addRandomSuffix: true |
pages/api/file/image-upload.ts |
Vercel Blob with addRandomSuffix: true |
pages/api/file/s3/multipart.ts |
slugify() + teamId/docId directory structure |
lib/files/put-file-server.ts |
slugify() + generated document IDs (newId) |
lib/notion/utils.ts |
validateNotionMediaUrl() with domain allowlist, HTTPS enforcement |
All files use safe generated keys: {teamId}/{docId}/{uuid}-{sanitizedName}
- Never uses raw user-provided filenames as storage keys
- Original filename stored in Document model metadata only
- Keys are structured to prevent cross-tenant access
| Provider | Key Generation | Validation |
|---|---|---|
| Replit Object Storage | {PRIVATE_OBJECT_DIR}/documents/{docId}/{safeName} |
UUID prefix, slugified name |
| S3/R2 | {teamId}/{docId}/{slugifiedName} |
Team isolation, slugified name |
| Vercel Blob | Auto-generated with random suffix | SDK-managed |
| Local Filesystem | Path canonicalization + boundary check | Throws on traversal attempt |
- Filename sanitization: Non-alphanumeric characters replaced with underscores or slugified via
@sindresorhus/slugify - Content-type validation: Based on file extension and MIME type with allowlists
- Encrypted storage: Enabled by default when
STORAGE_ENCRYPTION_KEYis configured - Size limits: Enforced per upload endpoint (100MB max for documents, 5MB for images)
- Allowed file types: Strict MIME type allowlists per endpoint
- Virus scanning integration (ClamAV or cloud-based)
- File content verification beyond extension/MIME type
- Magic byte validation for uploaded files
Implemented:
- Accreditation self-certification wizard
- Persona KYC/AML integration hooks (sandbox/production modes)
- Audit logging for verification steps
Audit Trail Contents:
- Timestamp (UTC)
- User ID
- Action type
- IP Address
- User Agent
Recommended retention periods for compliance:
| Data Type | Recommended Retention | Notes |
|---|---|---|
| Audit Logs | 7 years | SEC/FINRA compliance |
| Investor Documents | Fund lifetime + 7 years | Legal/tax requirements |
| Session Data | 30 days | Automatic expiry |
| KYC/AML Records | 5 years post-relationship | AML regulations |
Note: Actual retention is dependent on database backup and deletion policies configured by the operator.
- Run
npm auditbefore deployments - Enable Dependabot or similar for automated alerts
- Review and update dependencies monthly
- Critical: Patch within 24 hours
- High: Patch within 7 days
- Moderate: Patch within 30 days
- Low: Patch in next release
- All changes should require pull request review
- Security-sensitive changes should have additional scrutiny
- No secrets in code repository
- Use environment variables for configuration
- Use Replit Secrets or similar secure storage for sensitive values
| Secret | Rotation Frequency | Impact |
|---|---|---|
NEXTAUTH_SECRET |
Quarterly | Invalidates all sessions |
STORAGE_ENCRYPTION_KEY |
Annually | Requires re-encryption |
| Database password | Quarterly | Connection string update |
PLAID_SECRET |
As needed | Regenerate in dashboard |
STRIPE_SECRET_KEY |
As needed | Regenerate in dashboard |
PERSONA_API_KEY |
As needed | Regenerate in dashboard |
Rotation Procedure:
- Generate new secret value
- Update in environment (Replit Secrets or
.env.local) - Deploy/restart application
- Monitor logs for authentication errors
- Document rotation date
Rate limiting is implemented using lib/security/rate-limiter.ts with three tiers:
| Limiter | Limit | Window | Use Case |
|---|---|---|---|
authRateLimiter |
10 requests | 1 hour | Authentication endpoints |
strictRateLimiter |
3 requests | 1 hour | Sensitive operations |
apiRateLimiter |
100 requests | 1 minute | General API endpoints |
Protected Endpoints:
/api/auth/admin-magic-verify- Admin magic link verification (auth limiter)/api/view/verify-magic-link- Magic link verification (auth limiter)/api/request-invite- Invite requests (strict limiter)/api/lp/complete-gate- LP onboarding (api limiter)/api/sign/*- Signature endpoints (10-30 req/min via Redis ratelimit)
Violations are logged to the SignatureAuditLog table for security monitoring.
For production deployments requiring cross-origin API access, add CORS headers in next.config.mjs:
// Example configuration - add to next.config.mjs if needed
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: process.env.NEXTAUTH_URL },
{ key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE,OPTIONS' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type,Authorization' },
{ key: 'Access-Control-Max-Age', value: '86400' },
],
},
];
}Note: The application currently uses same-origin requests. Configure CORS only if you need cross-origin API access.
Database Backups:
- Daily automated backups (retain 30 days)
- Weekly backups (retain 1 year)
- Point-in-time recovery enabled
File Storage Backups:
- S3/R2: Enable bucket versioning
- Local: Daily compressed archives
Backup Verification:
- Monthly restore tests
- Documented recovery procedure
See docs/DEPLOYMENT.md for detailed backup scripts.
The following security improvements are recommended but not yet implemented:
- Virus Scanning: Integrate file scanning for uploads
- Client-Side Encryption: Encrypt sensitive data before transmission
- Immutable Audit Logs: Use append-only storage for compliance
- Penetration Testing: Conduct quarterly security assessments
For security inquiries:
- Email: [email protected]
- Response time: 48 hours
For general support:
- Email: [email protected]