Security
How LinkDen protects your data with input sanitization, secure setup, and authentication.
Security
LinkDen includes built-in protections against common web attacks. Most security measures work automatically -- this page explains what's covered and what you can configure.
First-User-Is-Admin
LinkDen uses a simple, secure setup model:
- Navigate to
/admin/setupafter deployment to create your admin account. - The first user to register becomes the admin.
- After the first user is created, registration is permanently locked -- the API returns 403 "Registration is closed" for any subsequent signup attempts.
- If you visit
/admin/loginbefore an account exists, you'll be redirected to the setup page.
This eliminates the need for setup tokens or invitation codes while ensuring only you can create the admin account.
File Upload Restrictions
Image uploads are validated server-side:
- Max file size: 5 MB — enforced from the
Content-Lengthheader before the body is buffered, so oversized uploads are rejected without loading them into memory. - Allowed extensions: jpg, jpeg, png, gif, webp, ico
- Allowed MIME types: image/jpeg, image/png, image/gif, image/webp, image/x-icon, image/vnd.microsoft.icon
- Magic-byte check: the file's actual signature must match the claimed extension — an HTML or script file renamed to
.pngis rejected even if its client MIME saysimage/png.
Uploads that fail validation are rejected with descriptive error messages (400 for invalid type or content mismatch, 413 for oversized files). When an image is replaced, the previous R2 object is deleted; a daily retention sweep removes any images no longer referenced by a setting or block.
Settings Key Allowlist
The settings API only accepts known setting keys. Attempts to read or write arbitrary keys are rejected by Zod validation. This prevents attackers from injecting unexpected configuration values even if they obtain a valid session.
Input Sanitization
LinkDen sanitizes all user-provided content on the server side before saving it. This protects against stored XSS attacks even if someone bypasses frontend validation.
What gets sanitized:
- Text fields (profile name, bio, titles, etc.) -- HTML tags are stripped and length limits are enforced.
- URLs -- Validated to ensure they use
http://orhttps://protocols. Dangerous protocols likejavascript:are rejected. - Colors -- Must match
#RRGGBBhex format. - Custom CSS -- Script tags,
expression()calls,javascript:URLs,@importdirectives,data:URLs, and backslash escape sequences are blocked. - Contact form submissions -- All text fields are stripped of HTML before storage.
- Embed URLs -- Validated against platform-specific patterns (e.g., YouTube, Spotify, SoundCloud).
Authentication
LinkDen uses Better Auth for authentication with email and password.
- Sessions are signed with the
BETTER_AUTH_SECRETenvironment variable. - All admin routes require a valid session.
- Public routes are limited to page data, contact submissions, analytics tracking, vCard downloads, and setup.
CORS
The API server checks the request origin against the CORS_ORIGIN environment variable. Only requests from your configured frontend URL are accepted.
Make sure CORS_ORIGIN matches your frontend URL exactly (including protocol, no trailing slash).
CAPTCHA
The contact form supports optional CAPTCHA verification via Cloudflare Turnstile or Google reCAPTCHA. When configured, tokens are validated server-side before accepting submissions. See Contact Form for setup details.
Rate Limiting
When deployed to Cloudflare Workers, LinkDen uses Cloudflare Rate Limiting (via Alchemy bindings) to protect all API endpoints. Four independent rate limit buckets are applied:
| Binding | Protects | Purpose |
|---|---|---|
RL_AUTH | /auth/* routes | Prevents brute-force login and password-reset attacks |
RL_STRICT | Sensitive mutations | Extra protection for admin write operations |
RL_UPLOAD | /api/upload | Limits file upload frequency |
RL_PUBLIC | Public page + analytics | Prevents scraping and analytics flooding |
Rate limit thresholds are configured in packages/infra/src/alchemy.run.ts and apply automatically on deployment. No additional configuration is needed. Limits are not active in local development.