The Nojoin team and community take all security vulnerabilities seriously. Thank you for your efforts to improve the security of Nojoin. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions.
Nojoin is still in active development and all releases should be considered pre-release. There may be security vulnerabilities in the application. Nojoin's maintainers are not responsible for any data loss or security breaches that may occur as a result of using the application. We also advise users to take additional security measures in general but especially when deploying Nojoin over a publically accessible URL. For example, we recommend using a VPN or a reverse proxy to secure your Nojoin instance.
Nojoin requires an operator-defined FIRST_RUN_PASSWORD before the first successful system initialisation can occur.
- The bootstrap password is only used while the system is uninitialised.
- The web client sends the bootstrap secret via the standard
Authorizationheader using theBootstrapscheme rather than a bespoke setup header. - If
FIRST_RUN_PASSWORDis missing, first-run setup fails closed until the operator sets it and restarts or redeploys. - After initialisation, normal authenticated admin operations do not use the bootstrap password path.
- The bootstrap password is never returned by the API or persisted into Nojoin configuration.
- Application log output redacts
Authorization, cookies, bootstrap credentials, passwords, tokens, and API-key fields if they are accidentally included in a log record.
Operators should treat FIRST_RUN_PASSWORD as a secret and ensure reverse proxies, ingress layers, and HTTP logging do not record Authorization headers or setup request bodies.
Nojoin's normal browser session uses a Secure HttpOnly cookie, but state-changing browser requests are not trusted solely because that cookie is present.
- Cookie-authenticated browser requests using unsafe methods must come from the trusted Nojoin web origin.
- The backend validates the standard
Originheader, or falls back toRefererwhen needed, for those cookie-authenticated unsafe requests. - Explicit bearer-token API clients are not subject to that browser-origin check.
- The session cookie remains
SameSite=Laxto preserve expected top-level redirect flows such as OAuth callbacks, so unsafe GET-style side effects should continue to be avoided.
Standard browser session and explicit API JWTs (token types session and api) support active invalidation in addition to natural expiry.
- Every issued session/API JWT carries a unique
jti(token id), aniat(issued-at) timestamp, and atvclaim that mirrors the user's currenttoken_version. - Verification rejects a token whose
tvno longer matches the user record. Bumpingtoken_versiontherefore acts as an immediate kill-switch for every previously issued session and API token belonging to that user. token_versionis bumped automatically when the user changes their own password, when an admin resets the password, and when an account is deactivated.- Admins and Owners may also call
POST /api/v1/users/{user_id}/sessions/revoke-allto forcibly invalidate every session for another user. Users themselves may callPOST /api/v1/users/me/sessions/revoke-allto sign out of all other devices. - Calling
POST /api/v1/login/logoutrecords the cookie token'sjtiin a server-side denylist (revoked_jwts), so the captured JWT stops verifying immediately even if a copy was made outside the browser cookie. - The denylist also supports targeted revocation of an individual
jti. Expired entries are pruned opportunistically. - Companion JWTs are not in scope for this mechanism. They are already revocable through the Companion pairing record and per-pairing
secret_versiondocumented above.
JWT signing material is stored as a small keyring rather than a single static value.
- The keyring is persisted to
<user_data>/.secret_keys.jsonand contains anactivekey id (kid) plus any prior keys that are still trusted. - Every issued JWT carries a
kidheader. Verification picks the matching key from the keyring; if thekidis not known the token is rejected. - Operators with shell access can rotate the key by calling
backend.core.security.rotate_signing_key(). Rotation generates a freshkid, makes it active, and (by default) keeps the previous key in the ring so currently outstanding tokens keep verifying until their natural expiry. - After enough time has passed for outstanding tokens to expire,
prune_signing_keys()removes retired keys from the keyring. Any token still signed by a removed key fails verification immediately, providing a hard cut-over. - Setting the
SECRET_KEYenvironment variable overrides the keyring with a single static key (intended for advanced deployments and tests). In that mode the rotation API is disabled. - Existing single-key installs are migrated automatically: the legacy
<user_data>/.secret_keyfile is loaded into the keyring askid="legacy"on first startup and the legacy file is renamed.
The Nojoin Companion app requires a strict manual pairing workflow.
- The Companion exposes a short-lived local API only for authenticated requests.
- Anonymous discovery of the Companion via the loopback interface is explicitly blocked. The frontend cannot silently detect if the Companion is running.
- First-run orientation, pairing initiation, repair, Firefox support, low-frequency utilities, and disconnect remain native-owned. The browser can collect the current pairing code and show coarse state, but it cannot silently start pairing, execute repair, or disconnect the backend on its own.
- Pairing must be manually initiated by the user from within the Companion app through
Start PairingorGenerate New Pairing Code, which generates a single-use, short-lived 8-character pairing code. - Firefox support remains explicit opt-in. Users must run
Enable Firefox Support, enable Firefox enterprise roots, restart Firefox, and use a fresh pairing code before Firefox-based local control will work reliably. - During pairing, the Companion captures and pins the first backend TLS certificate it sees for that backend target.
- The backend returns a revocable companion credential and a short-lived local control secret during pairing. The backend stores only a hash of the companion credential.
- On Windows, the Companion stores those secrets in a DPAPI-protected sidecar file rather than in
config.json. - The browser never receives a reusable Companion bearer token. The Companion exchanges its stored credential for a short-lived backend access token when it needs to call the backend.
- Re-pairing to a different Nojoin backend replaces the previous trust relationship atomically.
- After pairing, all Companion-to-backend HTTPS traffic requires the pinned backend certificate. If the backend certificate changes, the Companion must be explicitly re-paired.
- Browser repair remains a native-only action. Other surfaces may route the user to
Open Settings to Repair, but the actualRepair Local Browser Connectionaction runs inside Companion Settings. - Explicitly disconnecting the current backend from Companion Settings clears the saved backend certificate pin and local secret bundle, then attempts a best-effort remote revoke before returning the app to a clean first-pair state.
- All requests to the Companion's local API require a short-lived local control token and strict Host validation (e.g.
127.0.0.1orlocalhost). - Outbound Companion-to-backend HTTPS calls are constrained against server-side request forgery (SSRF):
- The pairing payload's
api_protocol://api_host:api_portmust equal the browserOriginheader on the/pair/completerequest, which is the operator-configuredWEB_APP_URL. This is a strict allowlist of one backend per pairing. - The protocol must be
https. The host is rejected if it is empty, longer than 253 characters, contains embedded credentials, paths, queries, fragments, or whitespace, or ifreqwest::Url::parsefails to round-trip it. - Each outbound request resolves the target host once via the system resolver and pins the resulting socket addresses into the
reqwestclient throughresolve_to_addrs. This closes the DNS-rebinding window between validation and connection.
- The pairing payload's
Operators and users should be aware that switching between deployments requires an explicit re-pair. Legacy plaintext Companion trust state is intentionally dropped by the current security upgrade, so existing installations must pair again after updating.
For end-user install, pairing, repair, and browser setup steps, see COMPANION.md.
As Nojoin is in active development, only the latest version is supported. We encourage all users to use the most up-to-date version of the application.
| Version | Supported |
|---|---|
| latest | ✅ |
Please report any security vulnerabilities.
You should expect a response within 48 hours. If the issue is confirmed, we will release a patch as soon as possible, depending on the complexity of the issue.