Please report security issues privately. Do not open a public issue.
- Use GitHub's private vulnerability reporting on this repository, or
- email the maintainers (see the repository owner profile).
We aim to acknowledge reports within 3 business days and to ship a fix or mitigation for confirmed high-severity issues within 30 days.
- The
identity-registrySoroban contract (packages/contracts/) - The web application and its API (
apps/web/) - The indexer (
apps/indexer/)
SIGNET_AUTH_SECRETmust be set (≥16 random chars) in production — the app refuses the dev fallback whenNODE_ENV=production. Rotate it to invalidate sessions going forward.
Sessions are stateless HMAC cookies with a seven-day lifetime, so revocation is an explicit act. Three levers, narrowest first — reach for the global one only when the thing that leaked is global.
One session. POST /api/auth/logout revokes the session id it is signing
out, so a cookie copied off the device before sign-out stops working too. An
operator can revoke a session id directly:
pnpm --filter @signet/web run revoke:sessions -- --session <session id>One address. The response to a single compromised wallet:
pnpm --filter @signet/web run revoke:sessions -- GABC…Every session for that address stops working; every other user is unaffected.
The wallet can sign in again immediately — this is a revocation, not a ban.
Users can do the same for themselves from Settings → Other devices, which
signs out every device except the one they are on
(POST /api/auth/revoke {"scope":"others"}, or {"scope":"all"} to include
the current one).
Both are recorded in the shared store (see UPSTASH_REDIS_REST_URL below) and
picked up by running instances within ten seconds — no restart, no redeploy, no
env change. Each instance caches the whole list in memory and refreshes it on
that interval, so this adds no per-request round trip. If the store cannot be
read, the last known list is used for up to a minute and after that every
session is rejected: an unreadable revocation list must not silently mean
"nobody is revoked".
Without
UPSTASH_REDIS_REST_URL/UPSTASH_REDIS_REST_TOKENthe list is per-instance, which is correct on a single long-lived server and wrong on a multi-replica or serverless deploy — a revocation would bind only on the instance that recorded it. Set them there. The CLI refuses to run without them rather than reporting a revocation the app never sees.
Everyone. SIGNET_SESSIONS_VALID_AFTER — set this to a Unix epoch
timestamp in milliseconds to reject all sessions issued before that moment.
This is the right lever for exactly one situation: SIGNET_AUTH_SECRET leaked
or was rotated, so every session really is suspect. The variable is checked on
every request, so no restart is needed.
export SIGNET_SESSIONS_VALID_AFTER=$(node -e "console.log(Date.now())")For Docker Compose deployments, set the variable in your environment or
.env file and recreate the containers:
SIGNET_SESSIONS_VALID_AFTER=$(node -e "console.log(Date.now())") docker compose up -d --force-recreate-
The default rate limiter is per-instance; back it with a shared store (
setRateLimitStore) for multi-instance deployments. -
Security headers (HSTS, …) are set in
apps/web/next.config.js; the CSP is built per request inapps/web/middleware.tssoscript-srccarries a fresh nonce instead of'unsafe-inline'. -
CSP violation reporting. The policy points
report-uriandreport-toatPOST /api/csp-report, which logs every violation as a structuredcsp.violationline. A blocked request is otherwise invisible — the page just renders with something missing — so watch that event after any policy change, and alert on it if you have a log pipeline. To send reports to an external collector instead, pass its URL asreportUritobuildCspin the middleware. -
The Identity Registry contract is immutable (no upgrade path) and uses a single admin key — use a multisig for the admin and audit before mainnet. A defect found after deployment cannot be patched; recovery is a new contract and a coordinated cutover, written up in advance in
docs/CONTRACT_MIGRATION.md. One of its preconditions — keeping binding snapshots, because Soroban RPC's event window is measured in hours — has to be in place before an incident, not during one.