This document outlines the security measures implemented in the TuvixRSS API and deployment considerations.
- Overview
- Authentication & Authorization
- Rate Limiting Strategy
- Input Validation
- Content Security
- CORS Configuration
- Environment Variables
- Security Checklist
- Security Scanning
- Reporting Security Issues
- Code References
- Additional Resources
TuvixRSS implements comprehensive security measures across authentication, authorization, input validation, content security, and deployment configurations. This guide covers all security-related features and best practices for secure deployment.
Related Documentation:
- Authentication Guide - Detailed authentication system documentation
- Rate Limiting Guide - Complete rate limiting system guide
- Session Management: HTTP-only cookies (more secure than JWT tokens)
- Expiration: Configurable via Better Auth settings
- Secret Requirements: Minimum 32 characters, stored in
BETTER_AUTH_SECRETenvironment variable
- Hashing: scrypt (Better Auth default, OWASP-recommended)
- Validation Requirements (Better Auth defaults):
- Minimum 8 characters, maximum 128 characters
- Better Auth uses standard password validation (no complex requirements by default)
- Custom validation can be added via Better Auth configuration if needed
- Token Generation: 32-byte cryptographically secure random token
- Token Expiration: 1 hour
- Security Features:
- One-time use tokens
- Automatic invalidation of previous unused tokens
- Tokens sent via email only (not in API responses)
- Email enumeration protection (always returns success)
TuvixRSS uses Resend for transactional email delivery, supporting email verification, password reset, and welcome emails.
For complete email system documentation, see Email System Guide.
Quick Setup:
- Create Resend account and verify domain
- Set
RESEND_API_KEYandEMAIL_FROMenvironment variables - Email templates use React Email components
- Development mode logs emails to console if API key is missing
Email Types:
- Email verification (when
requireEmailVerificationis enabled) - Password reset
- Welcome emails
See Email System Guide for:
- Complete setup instructions
- Email flow documentation
- Template development guide
- Troubleshooting guide
- API reference
All authentication events are logged with:
- User ID (if available)
- Action type (login, logout, password change, etc.)
- IP address
- User agent
- Timestamp
- Success/failure status
- Additional metadata
TuvixRSS uses a custom rate limiting system based on Cloudflare Workers rate limit bindings:
Better Auth rate limiting is disabled - we use custom Cloudflare Workers rate limit bindings instead.
Configuration: packages/api/src/auth/better-auth.ts
- Better Auth rate limiting: disabled (
rateLimit: { enabled: false }) - Custom rate limiting: Cloudflare Workers rate limit bindings (
API_RATE_LIMIT,FEED_RATE_LIMIT) - Docker Compose: Rate limiting disabled (all requests allowed)
TuvixRSS features admin-configurable rate limiting for API endpoints, stored in the database:
These settings control custom rate limiting logic (separate from Better Auth's built-in limits):
| Setting | Description | Default | Range |
|---|---|---|---|
maxLoginAttempts |
Failed login attempts before lockout | 5 | 1-100 |
loginAttemptWindowMinutes |
Time window for counting attempts | 15 minutes | 1-1440 |
lockoutDurationMinutes |
How long user is locked out | 30 minutes | 1-10080 |
Changing settings (Admin only):
// Via tRPC
await client.admin.updateGlobalSettings.mutate({
maxLoginAttempts: 10,
loginAttemptWindowMinutes: 20,
lockoutDurationMinutes: 60,
});Settings are cached for 1 minute to avoid database overhead on every request.
Authenticated API requests are rate-limited based on the user's plan:
| Plan | API Requests/Minute | Public Feed Requests/Hour |
|---|---|---|
| Free | Configurable | Configurable |
| Pro | Configurable | Configurable |
| Enterprise | Configurable | Configurable |
Managing plans (Admin only):
// Create new plan
await client.admin.createPlan.mutate({
id: "premium",
name: "Premium Plan",
maxSources: 500,
maxPublicFeeds: 10,
maxCategories: null, // unlimited
apiRateLimitPerMinute: 300,
publicFeedRateLimitPerMinute: 167, // ~10000/hour = ~167/minute
priceCents: 999,
features: "Unlimited categories, priority support",
});
// Update existing plan
await client.admin.updatePlan.mutate({
id: "free",
apiRateLimitPerMinute: 120, // Increase free tier limit
});
// Change user's plan
await client.admin.changePlan.mutate({
userId: 123,
plan: "premium",
});Admins can override plan limits for specific users:
await client.admin.setCustomLimits.mutate({
userId: 456,
apiRateLimitPerMinute: 1000, // Custom high limit
maxSources: 10000,
notes: "VIP customer - enterprise trial",
});The API uses in-memory rate limiting, which works well for:
- Single-instance deployments (Docker Compose)
- Development environments
- Low to medium traffic applications
If you scale horizontally (multiple API containers), each instance maintains its own rate limit state. An attacker could bypass rate limits by distributing requests across multiple instances.
For Docker deployments, add Redis for distributed rate limiting:
- Add Redis to docker-compose.yml:
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
api:
environment:
- REDIS_URL=redis://redis:6379- Install Redis client:
pnpm add ioredis rate-limiter-flexible- Update rate limiter (future implementation):
import Redis from "ioredis";
import { RateLimiterRedis } from "rate-limiter-flexible";
const redis = new Redis(process.env.REDIS_URL);
const rateLimiter = new RateLimiterRedis({
storeClient: redis,
points: 5, // Number of requests
duration: 15 * 60, // Per 15 minutes
});For Cloudflare deployments, use Cloudflare's built-in solutions:
A. Cloudflare Rate Limiting (Paid Feature)
- Configure via Cloudflare Dashboard
- Per-endpoint rate limits
- Automatic enforcement at the edge
- No code changes needed
B. Durable Objects (Alternative)
// Already configured in Env type
env.RATE_LIMITER; // Durable Objects namespaceDurable Objects provide:
- Strongly consistent state
- Global coordination
- Low latency
- Automatic scaling
C. Rate Limit Bindings (Rate Limiting)
// Already configured in Env type
env.API_RATE_LIMIT; // Rate limit binding
env.FEED_RATE_LIMIT; // Rate limit bindingUse for:
- API and public feed rate limiting
- Distributed edge-based rate limiting
- Lower cost than Durable Objects
| Deployment Type | Recommended Solution | Reason |
|---|---|---|
| Docker Compose (Single) | In-memory (current) | Simple, no dependencies |
| Docker Compose (Multi) | Redis | Distributed state |
| Cloudflare Workers | Cloudflare Rate Limiting or Durable Objects | Native edge integration |
| Kubernetes | Redis Cluster | Enterprise-grade scaling |
All user inputs are validated using Zod schemas with strict length limits:
USERNAME: { min: 3, max: 50 }
EMAIL: { min: 3, max: 255 }
TITLE: { min: 1, max: 500 }
DESCRIPTION: { min: 0, max: 5000 }
CONTENT: { min: 0, max: 500000 } // 500KB
URL: { min: 1, max: 2048 }
FILTER_PATTERN: { min: 1, max: 1000 }
CATEGORY_NAME: { min: 1, max: 100 }
OPML_CONTENT: { min: 1, max: 10000000 } // 10MB- Only HTTP/HTTPS protocols allowed
- Maximum length: 2048 characters
- Dangerous protocols blocked (javascript:, data:, vbscript:)
User-provided regex patterns are validated before execution:
- Maximum pattern length: 1000 characters
- Pattern compilation tested before use
- Consider adding ReDoS protection (safe-regex2) for production
All article descriptions are sanitized before storage to prevent XSS attacks while preserving safe formatting:
- Removes dangerous HTML tags and attributes (script, iframe, onclick, etc.)
- Preserves safe formatting tags (links, bold, italic, lists, headings, etc.)
- Enforces secure link attributes (target="_blank", rel="noopener noreferrer")
- Only allows safe URL protocols (http, https, mailto)
Implementation:
import { sanitizeHtml, truncateHtml } from "@/utils/text-sanitizer";
// Descriptions: Sanitized HTML (allows safe tags)
const sanitizedDescription = sanitizeHtml(rawDescription);
const description = truncateHtml(sanitizedDescription, 5000);Sanitization is enforced at the single entry point:
- Location:
packages/api/src/services/rss-fetcher.ts:689-693 - Library:
sanitize-htmlwith strict allowlist configuration - Allowed tags: Only inline elements (
a,strong,b,em,i,u,code,br) - Heading conversion:
h1-h6tags are automatically converted to<strong>to preserve emphasis - Blocked tags: Block-level elements (
p,blockquote,ul,ol,li,pre,div) are stripped - Allowed attributes: Only
href,title,target,relon links - Rationale: Descriptions render inside
<p>tags in the frontend, so only inline elements are valid HTML
Frontend rendering:
- The frontend uses
dangerouslySetInnerHTMLto render sanitized descriptions in two components:packages/app/src/components/app/article-item.tsx:238(standard article view)packages/app/src/components/app/article-item-audio.tsx:252(audio/podcast view)
- This is safe because sanitization is guaranteed at the backend ingestion layer
- Both components use the same
Articletype from tRPC, ensuring consistent data handling - No user-generated HTML is ever stored
- ✅ HTML sanitized from all article descriptions on ingestion
- ✅ Single code path for article creation (rss-fetcher.ts)
- ✅ Battle-tested sanitization library (sanitize-html)
- ✅ Strict allowlist of safe tags and attributes
- ✅ Zod validation on all user inputs
- ✅ Content type headers set correctly
- ✅ Drizzle ORM used for all database queries
- ✅ Parameterized queries throughout
- ✅ No raw SQL execution with user input
Set allowed origins via environment variable:
# Single origin
CORS_ORIGIN=http://localhost:5173
# Multiple origins (comma-separated)
CORS_ORIGIN=http://localhost:5173,https://example.com
# Development (defaults)
# If not set in development: http://localhost:5173, http://localhost:3000Behavior:
- ✅ Credentials enabled
- ✅ Origin validation on every request
- ✅ Blocked origins logged
⚠️ Production requires explicit CORS_ORIGIN
# wrangler.toml or secrets
CORS_ORIGIN=https://your-frontend.com
# Multiple origins
CORS_ORIGIN=https://app.example.com,https://beta.example.com
# Same-domain service bindings (if API and frontend on same domain)
CORS_ORIGIN=*For service bindings, consider using Cloudflare's built-in CORS settings instead of CORS_ORIGIN=*.
# Authentication (REQUIRED)
BETTER_AUTH_SECRET= # Generate with: openssl rand -base64 32
# CORS (REQUIRED in production)
CORS_ORIGIN= # Frontend URL(s)# Runtime
NODE_ENV=development # or "production"
PORT=3000
# Database
DATABASE_PATH=./data/tuvix.db # Node.js only
# Admin Bootstrap
ADMIN_USERNAME= # Auto-create admin user
ADMIN_EMAIL=
ADMIN_PASSWORD=
ALLOW_FIRST_USER_ADMIN=true # First user becomes admin
# Email (for password reset)
RESEND_API_KEY= # Email service API key
EMAIL_FROM= # From address
BASE_URL= # Base URL for reset links# Linux/macOS
openssl rand -hex 32
# Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Python
python -c "import secrets; print(secrets.token_hex(32))"Before deploying to production:
-
BETTER_AUTH_SECRETset to secure random value (32+ characters) -
CORS_ORIGINconfigured with specific frontend URLs -
NODE_ENV=productionset - Rate limiting strategy chosen and implemented
- HTTPS enabled (TLS/SSL certificates)
- Security audit logs monitored
- Email service configured (Resend API key and verified domain)
- Password reset email delivery tested
- Welcome email delivery tested
- Database backups configured
- Secrets stored in secure secret manager (not in git)
Run these tools regularly:
# Dependency vulnerabilities
pnpm audit
# Static analysis
npm install -g snyk
snyk test
# Secret scanning
docker run -v $(pwd):/path ghcr.io/gitleaks/gitleaks:latest detect --source=/path
# Container scanning
docker run aquasec/trivy fs .If you discover a security vulnerability, please email: [security contact - to be configured]
Do not open public issues for security vulnerabilities.
- Better Auth Configuration:
packages/api/src/auth/better-auth.ts - Password Security:
packages/api/src/auth/password.ts(admin init only) - Security Audit Logging:
packages/api/src/auth/security.ts - Email Service:
packages/api/src/services/email.ts - Email Templates:
packages/api/src/services/email-templates/ - Text Sanitization:
packages/api/src/utils/text-sanitizer.ts - Input Validation:
packages/api/src/db/schemas.zod.ts