If you discover a security vulnerability in LinkBreeze, please report it responsibly.
Do NOT open a public GitHub issue for security vulnerabilities.
Instead, please email: manak@omnirise.dev
Include:
- Description of the vulnerability
- Steps to reproduce (proof of concept if possible)
- Potential impact (who's affected, what an attacker could do)
- Suggested fix (if any)
- Acknowledgement: within 48 hours
- Status update: within 7 days (with severity assessment + fix plan)
- Patch release: within 30 days for high/critical, 90 days for moderate/low
- Public disclosure: after the patch is released, coordinated with reporter
We will not pursue legal action against researchers who:
- Make a good-faith effort to avoid privacy violations and service disruption
- Do not access or modify data that doesn't belong to them
- Report the vulnerability promptly and give us reasonable time to respond
- Do not publicly disclose the issue until a fix is available
LinkBreeze implements the following security practices:
- Auth: bcrypt password hashing (12 rounds), HMAC-signed session cookies
- Sessions: httpOnly, SameSite cookies, 30-day expiry, session invalidation on password change (stolen cookies become invalid when you change your password)
- Input validation: Zod schemas on every server action and API route
- SQL injection prevention: Drizzle ORM parameterized queries (no raw SQL)
- Security headers: HSTS, X-Content-Type-Options, Referrer-Policy, Permissions-Policy on all routes
- X-Frame-Options: SAMEORIGIN (prevents external clickjacking)
- URL validation: Rejects
javascript:,data:,blob:schemes - File uploads: Type whitelist (images only), size limit (2 MB), path-traversal-safe filename resolution
- Docker: Runs as non-root user (
node), data volume is the only writable path - CSRF protection: Next.js 16 Server Actions verify the
Originheader against theHostheader on every non-GET submission. Cross-site form posts are rejected automatically by the framework — no manual CSRF token needed. - Rate limiting: Login endpoint (5 attempts/min per IP), analytics tracking endpoint (60 req/min per IP), click redirect endpoint
/go/:id(60 req/min per IP), QR generation endpoint (30 req/min per IP) - SVG uploads blocked: SVG files are no longer accepted as uploads. The uploads serving route also sends
Content-Security-Policy: default-src 'none'andX-Content-Type-Options: nosniffheaders as defense-in-depth. - Privacy-respecting analytics: No raw IP storage — visitor hashes use SHA-256 (truncated to 64 bits) with a daily-rotating salt derived from the SECRET_KEY. The hash is one-way within the daily salt window. Important: hash reversal resistance depends on the SECRET_KEY remaining secret. Store the SECRET_KEY separately from the database (use an env var, not the auto-generated file in the data volume) for maximum protection. If an attacker obtains both the DB and SECRET_KEY, they could brute-force hashes for a specific day. The daily salt rotation means each day requires a separate brute-force pass. k-anonymity trade-off: the 64-bit truncation is deliberate — within a single day, multiple distinct IPs can map to the same hash, making it harder to isolate individual visitors at the cost of potential undercounting behind shared NATs with identical user-agents.
- No third-party requests on public pages: Favicons are fetched server-side and cached locally. Links without a cached favicon show a letter avatar instead of loading from a third-party domain. No visitor-facing request touches google.com or any analytics provider unless the operator explicitly configures external analytics.
- Referrer privacy: Only the origin of the Referer header is stored in analytics (e.g.,
https://instagram.cominstead of the full path with query parameters). - Privacy policy pages: Every public page auto-generates a privacy policy at
/{slug}/privacyfrom the page's actual configuration. Operators can customize it in Settings. Footer link is always visible.
The following are NOT yet implemented. If you need these, track the corresponding issue or contribute:
- Password recovery: No self-service reset flow. If you lose your password, see the Troubleshooting guide for three recovery methods (hash reset, full reset, or database deletion).
- 2FA / MFA (#5): Not yet available. Single-admin model with rate limiting (5/min/IP, 15/min global) is the current mitigation.
- Argon2id password hashing (#76): Currently uses bcrypt (12 rounds). Argon2id migration with transparent re-hash on login is planned.
- Email capture consent (#75): The subscriber form has no consent checkbox or privacy notice. Operators collecting emails from EU visitors should add their own consent mechanism until this is built in.
- Analytics retention default: Configurable via Settings -> Data, but defaults to infinite (0). Operators should set a retention window (#77).
This policy covers the LinkBreeze application code in this repository. Third-party dependencies should be reported to their respective maintainers.
LinkBreeze is self-hosted software. The operator (the person who deploys and runs the Docker container) is the data controller under GDPR, CCPA, and similar regulations. OmniRise (the developers of LinkBreeze) does not process, store, or have access to any data from your deployment. This is the same legal posture as WordPress, Ghost, or self-hosted Plausible.
Visitor data (collected automatically on public pages):
| Data point | Storage | Pseudonymized? | Retention |
|---|---|---|---|
| Page views + clicks | SQLite DB | Yes (visitor hash) | Configurable, default infinite |
| Visitor identifier | SHA-256 hash (64-bit, daily salt) | Yes | Rotates daily |
| Device type | mobile / desktop / tablet | Yes (categorized) | Same as analytics |
| Country | Derived from IP via local GeoIP DB | Yes (no city/region) | Same as analytics |
| Referrer origin | Origin only (no path/query) | N/A | Same as analytics |
The visitor hash is SHA-256(ip | user-agent | daily-salt), truncated to 64
bits. Raw IPs are never stored. User-agents are categorized to device type and
not stored individually. Referrers are stripped to origin only.
Operator data (collected during setup):
| Data point | Storage | Encryption |
|---|---|---|
| Admin password | SQLite DB | bcrypt hash (12 rounds) |
| Admin email | SQLite DB | Plaintext (for notifications) |
| Page content (title, bio, links) | SQLite DB | N/A |
| Subscriber emails (if email capture enabled) | SQLite DB | Plaintext |
LinkBreeze uses one cookie: the admin session cookie (lb_session). It is
httpOnly, SameSite, and required for authentication. It is only set after login
and is never placed on public pages.
Public pages use zero cookies. The built-in analytics are cookieless. If an operator enables external analytics (Google Analytics, Plausible, etc.) via the analytics script injection feature, that provider may set its own cookies on public pages. This is the operator's responsibility.
Operators may receive requests from visitors exercising their GDPR rights. Here is how LinkBreeze supports each:
- Right of access (Art. 15): The visitor hash is derived from the visitor's IP and user-agent. The operator cannot look up a specific visitor's data without knowing their IP and user-agent at the time of visit. The admin dashboard shows aggregated analytics, not individual visitor records.
- Right to erasure (Art. 17): Analytics data can be bulk-deleted via the Data settings (Settings -> Data -> Clear analytics). There is no per-visitor deletion mechanism because individual visitors cannot be reliably identified from the hash alone.
- Right to portability (Art. 20): Subscriber emails can be exported to CSV. Analytics data is aggregated, not per-visitor, so portability does not apply in the traditional sense.
The built-in email capture form does not currently include a consent checkbox, privacy notice, or purpose disclosure (tracked in #75). Operators collecting emails from EU visitors must ensure they have a lawful basis under GDPR Article 6 and meet the consent requirements of Article 7 until built-in consent tooling ships.
Every public page has an auto-generated privacy policy at /{slug}/privacy.
The policy reflects the page's actual configuration (analytics, email capture,
embeds, external scripts, retention window) so it stays accurate without
manual maintenance. A "Privacy" link appears in the footer of every public
page.
Operators can override the auto-generated text with a custom privacy policy via Settings -> General -> Privacy policy. When set, the custom text replaces the generated template entirely. The privacy page inherits the page's theme (colors, fonts, background) so it looks consistent.
The privacy policy route is noindex (not indexed by search engines).
When an operator adds a YouTube, Spotify, Vimeo, SoundCloud, or Bandcamp embed
to their page, the embed provider loads its own scripts and may set cookies or
collect visitor data. YouTube uses youtube-nocookie.com which limits but does
not eliminate Google tracking. Operators should assess whether embeds require
consent under the ePrivacy Directive and provide appropriate notices.
LinkBreeze does not transmit data to any external server. If the operator's deployment is compromised, the breach surface is limited to the operator's own server and database. Breach notification obligations fall on the operator under GDPR Article 33 (72 hours to the supervisory authority).
- Set SECRET_KEY as an env var — do not rely on the auto-generated key in the data volume. If the database volume is compromised, the SECRET_KEY should not be on the same volume.
- Set an analytics retention window — Settings -> Data -> Analytics retention. 90 days is a reasonable default. Infinite retention increases your compliance surface.
- Terminate TLS — Use a reverse proxy (Caddy, Nginx, Cloudflare Tunnel) for HTTPS. Without TLS, session cookies and admin credentials transit in plaintext.
- Regular database backups — Use the backup/restore feature in Settings -> Data. Store backups off-server.
- Restrict network access — Do not expose the SQLite port. Only expose the LinkBreeze HTTP port through a reverse proxy.
LinkBreeze is built on shadcn/ui (Radix Primitives), which provides WAI-ARIA compliant components out of the box:
- Keyboard navigation (Tab, Enter, Escape) works on all interactive elements
- Focus indicators are visible on all form fields, buttons, and dialogs
- Drag-and-drop links are keyboard-accessible via alternative controls
- Color themes meet WCAG AA contrast ratios
- Screen-reader-friendly markup (aria-labels, semantic HTML, role attributes)
The public link page is designed to be fully accessible — it's a simple list of links that works without JavaScript enabled.
If you encounter an accessibility issue, please open a bug report with the Accessibility label.