A simple, yet effective Personal Relationship Manager
  • TypeScript 98.6%
  • Shell 0.8%
  • JavaScript 0.3%
  • CSS 0.2%
Find a file
Matias Godoy 0216668807
fix(security): resolve the client IP from a declared trusted proxy (#450)
## What

Rate limiting derived the client IP from proxy headers without any
notion of which proxy, if any, was trusted to set them. Because those
headers arrive with the request, the value used as the rate-limit bucket
key was not reliably the caller's. This makes every limit in the app
weaker than intended: login, registration, password reset, email
verification, CardDAV, error reporting, push subscription, and the
notification endpoints.

This adds a trust boundary, de-duplicates the three copies of the
resolution logic, and stops the limits resting entirely on the IP.

## Upgrade notes

No action required. The defaults match both reverse proxy configurations
in our own documentation, so existing deployments behave the same after
upgrading.

Two new optional settings, both documented in
`self-hosting/configuration.md` and `self-hosting/reverse-proxy.md`:

- `TRUSTED_PROXY_COUNT` (default `1`): how many proxies sit in front of
Nametag. Set `0` if none, `2` for a CDN in front of a load balancer.
- `TRUSTED_PROXY_HEADER` (default `x-forwarded-for`): which header your
proxy sets. Set `x-real-ip` if your proxy sets that one instead.

## Why two settings rather than one

The two topologies cannot be told apart at runtime, and each makes the
*other* header untrustworthy:

- A proxy that sets `X-Forwarded-For` (our documented nginx and Caddy
configs both do) passes any client-supplied `X-Real-IP` straight
through.
- A proxy that sets only `X-Real-IP` passes any client-supplied
`X-Forwarded-For` straight through.

Preferring either header by default is therefore wrong for somebody, and
wrong silently. So the operator declares it, with the default matching
what our own documentation prescribes.

## How the resolution works

- With `x-forwarded-for`, entries are counted from the **right**. The
documented nginx config uses `proxy_add_x_forwarded_for`, which appends,
so anything a client supplied stays to the left of what the proxy adds.
- With `x-real-ip`, that header is read alone. It carries a single value
the proxy replaces rather than appends, so the hop count does not apply.
- The result must parse as an IP address, otherwise arbitrary text
becomes a bucket key.
- A count of `0` ignores both headers entirely.
- When no trustworthy IP can be determined the resolver returns `null`
rather than a placeholder, and the app logs a warning naming both
settings, because a silently shared bucket is how this stays broken.

There is an inherent limit worth stating: with one trusted proxy, a
single-entry header is indistinguishable from a compliant proxy's single
appended hop. Hop counting is only sound if the app cannot be reached
except through the proxy. That precondition is now stated in the
documentation, with a commented `127.0.0.1` binding in
`docker-compose.yml` for operators who front the app with a proxy.

## Cloudflare

`TRUSTED_PROXY_HEADER` also accepts `cf-connecting-ip`. Cloudflare
**overwrites** that header with the visitor address on every request,
rather than appending the way it does with `X-Forwarded-For`, so in that
mode there is no chain to count and no hop number to get wrong when a
layer is added or removed.

**That mode moves the entire security boundary to your origin.** With
`x-forwarded-for`, a wrong hop count degrades gracefully: an attacker
supplies one forged entry among several and the resolver usually still
lands on a real address. `CF-Connecting-IP` has no such cushion. Anyone
who can reach the origin without passing through Cloudflare can set that
header to anything, and nothing contradicts them. Use it only with the
origin locked down, by restricting the firewall to Cloudflare's
published ranges, enabling Authenticated Origin Pulls, or running a
Cloudflare Tunnel so the origin has no public address at all.

If you would rather not depend on that, leave the header at its default
and set `TRUSTED_PROXY_COUNT` to the number of appending hops instead:
`2` for Cloudflare in front of your own reverse proxy, `1` if Cloudflare
reaches the app directly.

Do **not** use `x-real-ip` behind Cloudflare. An origin Nginx setting
`X-Real-IP $remote_addr` records the Cloudflare **edge** address, so
every visitor arriving through the same edge would share a single
rate-limit bucket.

## Trusted versus untrusted

`resolveTrustedClientIp` is for security decisions and returns `null`
when it cannot be sure. `getClientIp` keeps its previous best-effort
behaviour and is now documented as being for log lines only. Every
remaining caller of the latter was checked individually and is a logging
argument.

## Reducing the reliance on the IP at all

Three routes previously keyed on the IP alone. `forgot-password`,
`resend-verification` and `register` now also key on the normalised
email address, so a per-account bound holds even where the IP cannot be
trusted. These are the paths that send mail to an address supplied in
the request.

Each of them checks the limit twice: an unkeyed check before the body is
parsed, and an email-keyed check after. Without the first, a malformed
body would consume no limit at all.

`login` is deliberately unchanged: its body must reach the auth handler
intact, and a per-email key would not bound password spraying, which is
one password tried against many addresses. Per-account lockout already
covers repeated guesses against a single account. `reset-password` and
`verify-email` carry only a token, which would produce a fresh key per
guess.

## Testing

`npm run verify` passes: lint, typecheck, 3437 tests, production build,
plus the docs site build.

The resolver is covered against spoofed prefixes and rotation in both
modes, hop counts of 0, 1 and 2, chains shorter than the configured
count, non-address values, IPv6, whitespace and empty entries, and each
mode ignoring the other's header.
2026-08-28 23:41:24 +02:00
.devcontainer chore: Some improvements for devcontainers 2026-01-19 17:31:05 +01:00
.github fix(release): clarify that Docker users don't need to run migrations manually (#414) 2026-08-14 12:29:47 +02:00
.vscode fix: remove ESLint auto-fix-on-save from VS Code settings 2026-08-06 11:29:29 +02:00
app fix(security): resolve the client IP from a declared trusted proxy (#450) 2026-08-28 23:41:24 +02:00
components feat(notifications): add signed outgoing webhooks (#449) 2026-08-28 16:28:07 +02:00
docs fix(security): resolve the client IP from a declared trusted proxy (#450) 2026-08-28 23:41:24 +02:00
hooks feat(reminders): advance notice for important dates and an opt-in weekly digest (#406) 2026-08-14 00:27:10 +02:00
lib fix(security): resolve the client IP from a declared trusted proxy (#450) 2026-08-28 23:41:24 +02:00
locales feat(notifications): add signed outgoing webhooks (#449) 2026-08-28 16:28:07 +02:00
prisma feat(notifications): add ntfy channel and hardened outbound client (#435) 2026-08-28 11:38:41 +02:00
public feat(notifications): add web push as a reminder channel (#434) 2026-08-28 00:31:41 +02:00
scripts feat(notifications): add web push as a reminder channel (#434) 2026-08-28 00:31:41 +02:00
tests fix(security): resolve the client IP from a declared trusted proxy (#450) 2026-08-28 23:41:24 +02:00
types feat: add user photo support (upload, navbar, graph, relationships) 2026-03-05 10:49:38 +01:00
.auto-changelog chore: Remove double colons from changelog format 2026-01-03 00:05:47 +01:00
.dockerignore fix: Trying to reduce docker image size and build times 2026-01-09 23:03:58 +01:00
.env.example fix(security): resolve the client IP from a declared trusted proxy (#450) 2026-08-28 23:41:24 +02:00
.gitignore docs: scaffold Starlight documentation site 2026-07-20 17:52:56 +02:00
.impeccable.md UI audit: strip AI aesthetic, fix accessibility, normalize design tokens 2026-03-25 22:26:52 +01:00
.npmrc fix: pin npm to public registry and purge Spotify Artifactory references 2026-07-21 17:29:13 +02:00
.prettierrc.yaml style: add .prettierrc for consistent formatting 2026-01-24 21:56:55 +06:00
.release-please-manifest.json chore(master): release 0.61.0 (#431) 2026-08-26 13:49:15 +00:00
CHANGELOG.md chore(master): release 0.61.0 (#431) 2026-08-26 13:49:15 +00:00
CLAUDE.md docs: add breaking changes warning to release notes 2026-07-22 13:31:05 +02:00
CODE_OF_CONDUCT.md Initial commit 2026-01-02 15:41:30 +01:00
CONTRIBUTING.md docs: slim README and CONTRIBUTING to point to docs site 2026-07-20 18:47:11 +02:00
docker-compose.services.yml refactor: Many quality of life improvements for contributors and self-hosters: 2026-01-18 23:08:00 +01:00
docker-compose.yml fix(security): resolve the client IP from a declared trusted proxy (#450) 2026-08-28 23:41:24 +02:00
docker-entrypoint.sh fix: Fix for docker entrypoint with the new env variables for database. 2026-01-19 21:24:26 +01:00
Dockerfile fix(docker): install tzdata so TZ env var can resolve non-UTC zones 2026-04-22 17:41:46 +02:00
eslint.config.mjs fix(security): resolve the client IP from a declared trusted proxy (#450) 2026-08-28 23:41:24 +02:00
i18n.ts feat(i18n): add French (fr-FR) translation (#428) 2026-08-26 15:31:51 +02:00
instrumentation.ts fix: clear lint warnings and edge runtime instrumentation error 2026-05-04 16:07:57 +02:00
LICENSE Initial commit 2026-01-02 15:41:30 +01:00
next.config.ts feat(pwa): add service worker with offline fallback 2026-07-29 17:17:35 +02:00
package-lock.json feat(notifications): add web push as a reminder channel (#434) 2026-08-28 00:31:41 +02:00
package.json feat(notifications): add web push as a reminder channel (#434) 2026-08-28 00:31:41 +02:00
playwright.config.ts Initial commit 2026-01-02 15:41:30 +01:00
postcss.config.mjs Initial commit 2026-01-02 15:41:30 +01:00
prisma.config.ts Removed DATABASE_URL requirement 2026-02-17 23:21:26 +01:00
proxy.ts chore: remove unused pino-http, fix error key naming, log duration as number 2026-02-25 13:52:43 +01:00
README.md fix(i18n): use 'today' wording for same-day reminder emails (#432) 2026-08-26 13:46:09 +00:00
release-please-config.json fix: split release and Docker build into separate workflows 2026-02-26 13:12:13 +01:00
SECURITY.md Rebranded from NameTag to Nametag 2026-01-09 00:02:33 +01:00
tsconfig.json fix: exclude docs/ from root TypeScript compilation 2026-07-21 00:48:25 +02:00
vitest.config.ts Initial commit 2026-01-02 15:41:30 +01:00

Nametag

License: AGPL-3.0

⚠️ Active Development Notice

Nametag is under active development and may introduce breaking changes between releases. Please read the release notes carefully before updating to ensure a smooth upgrade process.

Nametag is a personal relationships manager that helps you remember the people in your life and how they're connected. Track birthdays, contact information, how people are connected, and visualize your network as an interactive graph.

Dashboard Dashboard with network overview and statistics

Try the hosted version →

Screenshots

View screenshots

Dashboard dark theme Dashboard light theme Main dashboard with a network graph of relationships


People dark theme People light theme People list page


Groups dark theme Groups light theme Groups page


Journal dark theme Journal light theme Journal page


Relationships dark theme Relationships light theme Relationships page

Features

  • Track people with flexible attributes (name, birthday, important dates, photos, and user-defined custom fields)
  • Map relationships between people with custom relationship types
  • Visualize your network with interactive force-directed graphs
  • Organize contacts into custom groups
  • Interactive map view with geocoded addresses and clustered markers
  • Journal for logging interactions and notes over time
  • Set reminders for important dates and staying in touch
  • CardDAV sync with Google Contacts, iCloud, Outlook, and Nextcloud
  • vCard and JSON import/export
  • Duplicate detection and contact merging
  • Global fuzzy search with accent-insensitive matching
  • REST API with OpenAPI documentation and token authentication
  • OIDC / SSO authentication support
  • Installable as a PWA
  • Trash with soft-delete and restore
  • Bulk operations (delete, group assignment, relationships)
  • Messaging app quick links (WhatsApp, Signal, Telegram, and more)
  • Full dark and light theme support
  • Multiple languages (English, Spanish, Japanese, Norwegian, German, Chinese, Italian, Russian, Dutch)
  • Mobile-responsive design
  • Multi-platform Docker support (AMD64 and ARM64)

Hosted vs Self-Hosted

We offer a hosted version at nametag.one with a generous free tier and affordable paid plans, which helps fund development. You can also self-host Nametag for free with unlimited contacts and complete data ownership.

See the installation guide in the docs to get started with self-hosting.

Quick Start (Self-Hosted)

mkdir nametag && cd nametag
# add a docker-compose.yml and .env file (see the installation guide)
docker compose up -d

Then visit http://localhost:3000. For the full installation guide, environment variables, email, OIDC, Redis, and reverse proxy setup, see the docs.

Read the full docs →

Contributing

We welcome contributions! See CONTRIBUTING.md for how to get involved, and the development setup guide for detailed instructions on running Nametag locally.

Roadmap

Contributions to any of these items are very welcome! Items that require the most help will have the [HELP NEEDED] tag. If you want to contribute to an item that does not have a PR or Issue associated to it, please create it yoursef.

To do

Future features and improvements, ordered by priority:

  • [HELP NEEDED] Mobile app (Native apps for Android and iOS are preferred)
  • Add support for SQLite databases
  • Add notification support [Issue #6]
  • Support multi-user groups [Issue #37]
  • Immich integration [Issue #46]
  • [HELP NEEDED] Additional language translations (Portuguese, Korean, etc.)

Done

Features and improvements that have already been implemented:

  • UI/UX improvements and accessibility enhancements
  • Documentation improvements (API, deployment, functionality, development, etc)
  • SMTP support [Issue #4, PR #21]
  • Option to disable registration [Issue #9, PR #17]
  • ARM build for docker images [Issue #14, PR #18]
  • Improve development setup to make contributors' lives easier [PR #25]
  • Implement CardDAV support [Issue #15, PR #82]
  • API for third-party integrations [Issue #29, PR #70]
  • Add photos to people [Issue #19, PR #135]
  • Add custom template titles for important dates [Issue #23, PR #176]
  • Add journaling capabilities [Issue #28, PR #192]
  • Implement OIDC [Issue #10]
  • Add map to show people's locations [Issue #26, PR #341]
  • Full-text fuzzy search [PR #301]
  • Configurable photo compression [PR #331]
  • HEIC photo support [PR #357]
  • Messaging app quick links [PR #375]
  • PWA / installable app with offline support [PR #379]
  • Duplicate detection and contact merging
  • Custom field templates
  • Trash with soft-delete and restore
  • Data import/export (JSON)
  • Bulk operations
  • Eastern name order support

License

Licensed under the GNU Affero General Public License v3.0. This ensures that if you modify and deploy Nametag, you must make your source code available.

Support

  • Hosted version: For support with the hosted service, email support@nametag.one
  • Self-hosting: Open an issue on GitHub, or check the docs
  • Security issues: See SECURITY.md

Support Development

If you find Nametag useful and want to support its development, you can buy me a coffee!

Buy Me A Coffee


Built with care for people who care about people.