Fix tenant isolation, default credentials, and resource limits - #1
Merged
Merged
Conversation
Client portal (reachable by hosting customers, panel runs as root): - Resolve tenant-supplied file paths through assertSafeFileTarget so a symlink planted in a docroot cannot redirect a read or write outside it. Applies to the file manager, uploads, error pages and the .htaccess editor. - Resolve account-namespaced names against every account with longest-prefix wins. Usernames may contain underscores, so `alice` and `alice_shop` can belong to different customers and prefix matching alone crossed tenants. - lstat the home and .ssh directories before writing authorized_keys, and reject multi-line keys. - Map each portal route to the feature-catalog key it belongs to instead of collapsing eight of them onto file-manager, and fail closed on unmapped routes. Stop treating req.params.id as an account id in the feature gate. Reseller tenancy: - Scope every /api/accounts route on accounts.reseller_id; out-of-scope ids answer 404. Stamp reseller-created accounts and enforce alloc_accounts. - Default a reseller with no reseller_privileges row to a customer-facing feature set rather than every key, and restrict settings writes to admins. Credentials and limits: - Remove the built-in 'changeme' fallbacks from admin login, change-password, security-extra, WordPress installs and mailing lists; require an explicit password instead. The admin one also ran a blocking bcrypt hash per request, which stalled the event loop under an unauthenticated login flood. - Refuse to start in production without a configured admin. - Bind to loopback by default and trust a single proxy hop, so a local client cannot forge X-Forwarded-For past the login rate limit. - Cap concurrent terminal sessions and per-message input; drop the ?token= query fallback that put a JWT in access logs. Also: look pm2 up on PATH instead of two fixed directories, make the test suite hermetic (no writes to /usr/local/bin, /var/backups or /var/lib), and upgrade dependencies so both audit gates pass. Adds regression tests for each boundary above and a stress harness (npm run stress) wired into CI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hardening pass across the client portal, reseller scoping, and credential handling, with a regression test for each invariant and a load/resilience harness.
Maintainers: I've prepared a detailed writeup of the issues behind these changes and can send it privately — the repo has no SECURITY.md or private reporting enabled, so please let me know the best contact. Happy to coordinate an advisory and hold further detail until you've cut a release.
Client portal
The portal is reachable by hosting customers while the panel process runs as root, so the boundaries below are the ones separating a tenant from the rest of the box.
assertSafeFileTarget. Since a tenant can write symlinks anywhere under their own docroot, the prefix check alone doesn't hold. Routed the file manager, uploads, error pages and the.htaccesseditor through the same helper._. Usernames may contain underscores, soaliceandalice_shopcan belong to different customers and that test is ambiguous.lstateach path component before writingauthorized_keys, and reject multi-line keys.cron,ssh-keys,scripts,subdomains,redirects,error-pages,htpasswd,ssl-advanced) were all mapped tofile-manager, which made them inert. Unmapped portal routes now fail closed instead of skipping the gate.req.params.idas an account id. On portal routes:idis an invoice / spam-rule / autoresponder id, so the check was being evaluated against an unrelated account.Reseller scoping
enforceResellerPrivilegeanswers which feature a reseller may use, never which accounts, so/api/accountsroutes operated across the whole server.accounts.reseller_idalready existed but was only read by the summary view./api/accountsroute is scoped onreseller_id; out-of-scope ids answer 404 rather than confirming they exist.resellers.alloc_accountsis enforced.reseller_privilegesrow defaults to a customer-facing feature set rather than every key.Credentials and limits
ADMIN_PASS_HASH || bcrypt.hashSync('changeme', 10)fallback from admin login,/change-passwordandsecurity-extra, and the equivalent password defaults in WordPress installs and mailing lists. An explicit password is now required. The admin path also ran a blocking hash per request, which stalls the event loop under a login burst.install.sh:687already states the process "binds to 127.0.0.1:3001 only", butlisten(PORT)with no host binds all interfaces.X-Forwarded-Forcan't displace the peer address the rate limiter keys on.?token=fallback so JWTs stay out of access logs.Tests, CI, and one unrelated fix
pm2is now looked up onPATHinstead of/usr/local/binand/usr/bin, which reported "pm2 is not installed" on any box using nvm or/opt./usr/local/bin,/var/backupsand/var/lib, so five tests passed only on the CI runner and failed for contributors. Now hermetic.npm auditprod / allserver/src/stress/adds a load and resilience harness (npm run stress) with its own CI job. Each scenario asserts a threshold rather than printing numbers, so regressions fail the build.Review notes
DEFAULT_RESELLER_FEATURESinfeature-lists.tsis my best guess at what a reseller needs — worth confirming it matches how you actually sell.accounts.reseller_idis NULL on current rows, so resellers see nothing until it's populated. That migration is deliberately not in this PR.