60 Commits
Author SHA1 Message Date
Jamie Curnow 66ab4b24d1 Stop non-admins injecting nginx directives through host fields
advanced_config is admin-only, but other fields are written into nginx
config as-is and the schema barely limited them. A non-admin could add
any directive through them:

- location path, forward_host and forward_path (any non-empty string)
- proxy host forward_host (a quote breaks out of set $server "...")
- redirection forward_domain_name and stream forwarding_host (their
  pattern allowed ";", e.g. "example.com; return 200; #")
- access list IPv6 addresses ("%.+" zone id allowed "::1%x; allow all")
- domain names (a newline got out of the header comment)

Validation
- New lib/nginx-values.js holds the rules. Values can't contain
  characters that end or change an nginx token: whitespace, control
  characters, ; { } " ' # \, and no $ in a host. Location paths allow a
  modifier and an unquoted or double-quoted URI, so =, ~, ~* and ^~
  locations and quoted regexes keep working.
- The API schemas use the same patterns, including the certificate
  schema's own copy of the domain rule. A test fails if they drift.
- generateConfig checks stored values again, so rows saved before these
  rules can't inject either. The check is about nginx structure only:
  a stored value the API would now reject, but which can't add
  directives, still renders. If a value is unsafe, nothing is written
  and the host is marked offline with the reason in nginx_err. The same
  check covers per-location access lists and the Let's Encrypt request
  config.
- htpasswd user names can't contain ":" or control characters, which
  corrupted the file.

Fixes along the way
- IPv6 client addresses: drop zone ids, which nginx's allow/deny
  rejects anyway, and fix broken escapes (literal "d" for digits, an
  unescaped ".").
- Rewrite the domain rule used by redirection and stream hosts so it
  can't backtrack exponentially. It accepts the same values.
- Pattern errors still say "must match pattern", but no longer quote
  the regex.

Tests
- backend/test/nginx-values.test.js, run by `yarn test` and in
  scripts/ci/test-and-build. Real request schemas reject an injection
  set and accept valid values on every endpoint. Accepted values,
  rendered through the real templates and parsed using nginx's
  tokenizer rules, keep the same statement structure. The check
  refuses unsafe stored rows.
- NginxValues.cy.js: injection attempts from a non-admin get 400, and
  hosts using valid edge-case values come up with nginx_online true.
2026-11-10 08:20:02 +10:00
Jamie Curnow 5b7e34eb67 Fix second round of review findings from PR #6029
Security
- Check advanced_config permissions before creating a certificate on
  host update (proxy, redirection and 404 hosts). A refused request
  previously requested a Let's Encrypt certificate first, leaving an
  orphan certificate and using up rate limit.
- Only trust X-NPM-Peer-Addr when it comes with a secret generated at
  container start. The admin nginx sends both headers. Any proxy host or
  stream forwarding to 127.0.0.1:3000 was also a loopback peer, so it
  could forge the header and get a fresh rate-limit bucket on every
  request. Without the secret (dev, or a mounted production.conf), the
  header is ignored.
- Add a login limit per IP across all accounts (30 failed attempts a
  minute) alongside the per-account one. Before this, one address could
  spray passwords over any number of accounts. Behind a shared proxy,
  everyone using that proxy shares this limit.
- Stop trusting X-Real-IP from loopback. It was only there for an older
  version of the rate limiter, and it let a stream host forwarding to a
  local proxy host forge the client IP and get past IP access lists.
  Loopback now behaves as it does on master.

Correctness
- Refuse to delete a certificate that hosts still use (400, with counts
  per host type). Their configs include its files, and a successful
  revoke deleted them anyway, which broke nginx -t. Otherwise the
  certificate is revoked and its files are removed even if the revoke
  fails.
- Let non-admins clear a location's advanced config. Deleting the
  location was already allowed and has the same effect, and a location
  deleted and re-added at the same path arrives with an empty config,
  which was refused.
- Clear proxy host grants (meta.proxy_host_ids) when visibility isn't
  "user" or proxy hosts are hidden, and drop ids of deleted hosts.
  Before, hidden grants came back when proxy hosts were shown again.
  The backend enforces this; the permissions modal also stops sending
  them.
- 404 host and redirection host modals now wait for the current user
  to load, so an admin's advanced config change isn't dropped if they
  submit before it loads.

Tests
- NonAdminHosts.cy.js: refused change doesn't create a certificate,
  location rename and clear rules, grant clearing.
- Certificates.cy.js: deleting a certificate that is in use.
2026-11-09 23:39:26 +10:00
Jamie Curnow 04c1f5ae76 Fix review findings from PR #6029
Security
- Redact certbot output before it reaches the API: strip the
  certificate's DNS credential values, URL query strings and userinfo,
  secret-looking key=value pairs and Bearer/Basic tokens. DNS plugins
  can echo request URLs carrying API keys. The raw CommandError is no
  longer attached as `previous`, since the error handler returns
  `previous` to the client on certificate routes.
- Rate limit login/2FA on the admin nginx's real TCP peer, passed as
  X-NPM-Peer-Addr ($realip_remote_addr), instead of X-Forwarded-For.
  nginx trusts X-Real-IP from private ranges, so a client on the LAN or
  Docker network could get a fresh bucket on every request. Behind
  another proxy, limits now apply per account through that proxy.
- Bound rate limiter memory: hash keys over 128 chars and cap each store
  at 50k entries, removing expired entries first and then the oldest.

Correctness
- Proxy host modal: send location advanced configs unchanged for
  non-admins instead of looking them up by path, which dropped the
  config from a renamed location.
- Location advanced config: locations that omit advanced_config now get
  a config only after explicit ones are matched, so a new location at
  an old path can't take the config from the renamed location.
- Certificate upload: check that the certificate matches the private
  key (uploaded or stored) before writing, so a mismatched pair can't
  break nginx -t.
- Certificate delete: keep the Let's Encrypt files when revoke fails and
  hosts still use the certificate, so their configs still load.

Cleanup
- Make the rate-limit certbot hint first in the list, as its comment
  says.
- Proxy host getCount uses the permissions access has already loaded
  instead of querying user_permission again.
- Rate limiter: register the success refund listener in one place and
  call it a fixed window, not a sliding one.
2026-11-09 22:53:53 +10:00
Jamie Curnow 87b77d9461 Optimise isAdmin lookups 2026-11-09 22:25:07 +10:00
Jamie Curnow 0cde59a8ad Fix for rate limiter only trusts X-Forwarded-For when the socket is
loopback, and the admin nginx sets that header to its own $remote_addr
2026-11-09 22:00:26 +10:00
Abdulhafiz Davlatov bc8bbf3562 feat: rate limit auth endpoints and scope user proxy host permissions
- Add in-memory sliding window rate limiter for auth token endpoints with CI/test bypass and successful request exclusion
- Add user_permission meta column migration and Objection model configuration
- Support scoping allowed proxy hosts per user when in 'Created Items Only' visibility mode
- Restrict advanced nginx configuration editing on hosts to admin users only
- Fix user permissions route ID parsing and omit id from permission update payloads
- Add allowed proxy hosts selector in permissions modal and display permissions summary badges in users table
2026-11-05 12:16:36 +05:00
Jamie Curnow 2cfd3395cf Adds integration tests for per path access lists, fixes ipv6 jsv,
and enforces host-wide access list when location access list is not set
2026-09-24 12:21:49 +10:00
carlosalbertorg 4282d6c6e9 feat: Add a Logs viewer to the admin UI
Nginx Proxy Manager had no way to inspect application or nginx logs
from the web UI - admins had to shell into the container or read
`docker logs`. This adds an admin-only Logs page that can tail:

- the backend application log, now also mirrored to
  /data/logs/backend.log (in addition to stdout) and rotated by the
  logrotate timer that already runs every 2 days
- the Let's Encrypt/certbot log, which certbot already writes to
  /data/logs/letsencrypt.log via its existing --logs-dir flag
- per-host nginx access/error logs (proxy, redirection, 404 and
  stream hosts), with the file path always resolved server-side from
  a validated host_type enum + numeric host_id, never from client
  input

Reads use a reverse chunked scan (64KB chunks, capped at 5MB scanned
per request) instead of loading whole files into memory, and the
frontend polls every 5s only while the tab is focused and "Live" is
on, so this stays cheap on both CPU and memory. No new runtime
dependencies were added on either side.

Purely additive: two new admin-only endpoints
(GET /api/logs/sources, GET /api/logs/tail), no existing behaviour
changed.
2026-09-22 13:11:38 -03:00
José M. Requena Plens e4585ac688 Stamp the password change from the app clock, not the database one
Your CI caught this: the check passed on SQLite and never fired on the
stack where the database container runs on a different timezone from the
app, so a stale token stayed valid. The comparison was between a token's
`iat`, which is UTC seconds from Node, and `auth.modified_on`, which the
driver hands back interpreted in the app's timezone. With the app on
Australia/Brisbane and the database on UTC, that column comes back ten
hours in the past and the token always looks newer than the change.

Record the moment in `auth.meta.password_changed_at` instead, written by
`setPassword` with the same `Date.now()` clock that mints `iat`. Same
unit on both sides, one clock, and no timestamp parsing: the Date and
local-string branch is gone, and so is the whole-second flooring that
Postgres microseconds made necessary.

Rows written before this have no marker and revoke nothing until their
next password change, which is the safe direction to be wrong in.
2026-09-06 20:15:12 +02:00
José M. Requena Plens 1ffe3609f4 Invalidate tokens issued before a password change
Tokens are stateless JWTs, so changing a password left every session that
the old one had opened working until its own expiry, up to a day later.
That is the case the password change is meant to close: an administrator
resetting a compromised account did not evict whoever was already in it.

The auth row already records when the password last changed, so no
migration is needed: `Access.init()` reads it alongside the user it
already loads and refuses a token whose `iat` is older. Both sides are
compared as whole seconds, which is all `iat` carries, so a token minted
in the same second as the change is kept. Postgres stores that column to
the microsecond, which is why the comparison is not done in milliseconds.

It is reported as 401 rather than the usual 403 because that is what the
frontend clears the session on, so the browser holding the dead token
lands on the login page instead of a page full of errors, and `can()`
lets that one error through unwrapped for the same reason.

Only the password does this. A user row changing (a rename, an avatar,
permissions) does not, and a user with no password auth row, which is
what a login through an external provider looks like, is not affected.
2026-09-06 19:48:02 +02:00
vgoer 29687987b2 fix: Restrict JWT key file permissions to 0600 2026-08-24 09:30:22 +00:00
Timothé Stoifl 246c54503e Merge branch 'develop' into claude/nifty-neumann-83e87c 2026-08-22 15:40:15 +02:00
Jamie Curnow 4e2b052b50 Updated backend packages 2026-08-22 23:10:26 +10:00
Timothé Stoifl ce4e35a3d4 Fix missing new on PermissionError in access.can()
The catch block in `access.can()` constructed `errs.PermissionError`
without `new`. The error constructors in `backend/lib/error.js` are plain
constructor functions that assign to `this` and return nothing, so calling
one without `new` evaluates to `undefined`.

The statement therefore did `throw undefined`, the express error handler
in `backend/app.js` received undefined (and could not read `.status` or
`.public` off it), and the request fell through to the catch-all 404
handler in `backend/routes/main.js`.

Net effect: every authorization failure raised by `access.can(...)` was
reported to clients as `404 Not Found` instead of `403 Forbidden`.

Line 45 of the same file already used `new` correctly, which shows this
was an oversight rather than deliberate resource-existence hiding. A grep
over `backend/` confirms this was the only error constructor invoked
without `new`.
2026-08-22 13:07:09 +02:00
Jamie Curnow 7e9cc32f97 Fix cypress suite
- split out cypress tests for plugins to be isolated
- use updated testca image from proper location
2026-05-27 07:25:24 +10:00
Jamie Curnow 03c70e3902 Fixes for upgraded debian trixie
- Adds request logging in debug mode for some endpoints
- Moves certbot version determination to the startup scripts and removes
  bash script encapsulation when installing plugins
- Revert loose domain validation, which was there for a specific reason
  addressing CVE's
- Fix Cypress suite for cert generation
- Adds Cypress test that iterates over the entire certbot plugins list
  and installs each one, ensuring at the very least that the install
  works
- Fixed some plugins based on this
- (!) Still some work to do on this, hostinger is still broken at least
- Improved cypress tests for custom certs; they will generate on each
  run instead of being baked in. The baked ones were due to expire soon
2026-05-25 07:37:24 +10:00
jc21 ebcd2a209c Merge pull request #5415 from 12LuA/develop
Fix typo in debug mode comment
2026-05-14 11:43:08 +10:00
Markus b66fd9d3ae Added PUT and DELETE method to Access-Control-Allow-Methods field 2026-05-12 09:24:28 +02:00
12LuA cafb614b80 Fix typo in debug mode comment 2026-03-18 13:47:03 +01:00
Jamie Curnow 0dbf268f37 Fix #5284 for older sqlite3 configurations 2026-02-18 08:32:17 +10:00
Jamie Curnow 77662b4e7f Use better-sqlite3 package for sqlite databases 2026-02-05 13:11:57 +10:00
Jamie Curnow 23f4948bde Bump version 2025-11-05 12:33:59 +10:00
Jamie Curnow f35671db21 Fix #4837 for those with older config 2025-11-05 10:56:23 +10:00
Jamie Curnow 8c37348b65 Properly wrap debug calls 2025-11-04 13:43:52 +10:00
Jonathon Aroutsidis 71a2277b9b Replace spaces with tabs 2025-11-03 10:48:14 +11:00
Jonathon Aroutsidis 5acf287ea7 Aligned Assignments and arrow-parens 2025-11-03 10:48:14 +11:00
Jonathon Aroutsidis e34206b526 Include SSL Options for MySQL 2025-11-03 10:46:20 +11:00
Jamie Curnow 74a8c5d806 Fix app crash when do unautorized things 2025-10-30 15:03:01 +10:00
Jamie Curnow 5b7013b8d5 Moved certrbot plugin list to backend
frontend doesn't include when building in react version
adds swagger for existing dns-providers endpoint
2025-10-26 00:28:03 +10:00
Jamie Curnow d0767baafa Proxy host modal basis, other improvements 2025-10-02 08:12:37 +10:00
Jamie Curnow 18537b9288 404 hosts add update complete, fix certbot renewals
and remove the need for email and agreement on cert requests
2025-10-02 08:12:37 +10:00
Jamie Curnow 1928e554fd Fix proxy hosts routes throwing errors 2025-10-02 08:12:28 +10:00
Jamie Curnow fb2708d81d Fix cypress tests following user wizard changes 2025-10-02 08:12:09 +10:00
Jamie Curnow 0b2fa826e0 Introducing the Setup Wizard for creating the first user
- no longer setup a default
- still able to do that with env vars however
2025-10-02 08:12:05 +10:00
Jamie Curnow 330993f028 Convert backend to ESM
- About 5 years overdue
- Remove eslint, use bomejs instead
2025-10-02 08:10:18 +10:00
Jamie Curnow 8c9d2745e2 Fix remote execution bug where email address can contain malicious code
also convert almost all cmd execs for certificates to properly escape arguments
2025-08-20 10:57:24 +10:00
Jamie Curnow 5f54490d86 Set SETUPTOOLS_USE_DISTUTILS for all plugin installs, seems like they all need it. 2025-07-09 12:35:20 +10:00
Jamie Curnow c97b8a339d Some auto formatting changes suggested by ide 2025-07-09 11:34:57 +10:00
Jamie Curnow ed1d90ee7f Fix powerdns dns plugin install, deps are outrageously old ;( 2025-07-09 11:34:19 +10:00
Jamie Curnow ca3ee98c68 Postgres Support
- Combines #4086 and #4087 PRs
- Adds authentik in CI stack
2024-12-24 16:48:48 +10:00
Jamie Curnow 929ac3bd7c Adds env var to set certbot acme server
this is required for test suite to use dns certbot request
without talking to live or staging letsencrypt servers or
production level dns providers. This is a backwards port
from the v3 branch and opens the door for a full certificate
cypress test
2024-10-16 11:06:29 +10:00
jc21 02a7b43932 Merge pull request #3991 from nlynzaad/develop_mysql2
swap mysql library and knex client for mysql2
2024-10-11 14:05:26 +10:00
Jamie Curnow 7c97516de6 Fix schema issue with cors 2024-10-10 16:31:19 +10:00
Jamie Curnow 4572b205c9 Openapi Schema improvements
- Return proper booleans in api responses
- Update jsonschemavalidation to latest draft
2024-10-10 15:53:11 +10:00
Jamie Curnow dfe2588523 Refactor API Schema and validation
- /schema now returns full openapi/swagger schema
- That schema is used to validate incoming requests
- And used as a contract in future integration tests
- Moved route files up one level
- Fixed incorrect 404 reponses when getting objects
- Fixed saving new objects and passing jsonschemavalidation
2024-10-09 18:05:15 +10:00
Nico Lynzaad 48a9f5f9db swop mysql library and knex client for mysql2 2024-09-10 23:08:02 +02:00
An Seongjin 001c77e686 Fix syntax that causes errors (generateKeys log) 2024-06-30 22:27:54 +09:00
Jamie Curnow da820db4e1 Fix startup hang due to unresolved promise
Affects instances where there are certs but none
of them are dns validated
2024-01-21 20:48:53 +10:00
Jamie Curnow 10d9760242 Refactor certbot plugin install for setup 2024-01-18 15:13:16 +10:00
Jamie Curnow db23c9a52f Refactor certbot plugins install
- Added a script to install every single plugin, used in development and debugging
- Improved certbot plugin install commands
- Adjusted some version for plugins to install properly
- It's noted that some plugins require deps that do not match other plugins,
  however these use cases should be extremely rare
2024-01-18 12:26:55 +10:00