Skip to content

fix(security): add /api/pxpipe to LOCAL_ONLY_PATHS — prevent unauthenticated RCE - #3078

Open
zmf963 wants to merge 1 commit into
decolua:masterfrom
HotSec:fix/pxpipe-local-only
Open

zmf963 wants to merge 1 commit into
decolua:masterfrom
HotSec:fix/pxpipe-local-only

Conversation

@zmf963

@zmf963 zmf963 commented Aug 6, 2026

Copy link
Copy Markdown

Summary

Consolidated security hardening for three related findings from #3049 (SSRF report with full PoC). All three were verified end-to-end on a fresh Pi 500 + Docker v0.5.50 deployment before writing these fixes.

1. SSRF via provider_options.baseUrl on /v1/search (searxng)

resolveBaseUrl() in open-sse/handlers/search/callers.js accepted a client-supplied provider_options.baseUrl override with no validation. Since searxng is a noAuth provider (no credentials required), any remote client could make the server issue GET requests to arbitrary internal/private/metadata addresses (e.g. http://169.254.169.254/latest/meta-data, http://10.0.0.1).

Fix: resolveBaseUrl() now rejects non-public URLs for client-supplied overrides via the existing assertPublicUrl() (same guard already used by /v1/fetch and provider-nodes validation). Only http:/https: protocols are accepted. The provider's own configured baseUrl (admin-controlled) remains trusted as-is.

2. Default-password remote login issues a valid JWT

On a fresh install (no password hash, INITIAL_PASSWORD unset), a remote client logging in with the default password 123456 received a valid dashboard JWT before any password change. The mustChangePassword flag was returned in the response body, but the JWT was already set — so a remote attacker could immediately PATCH /api/settings to disable requireLogin entirely (CVE-2026-56679 class attack chain).

Fix: When mustChangePassword is true (fresh install + remote + default password), the login route now returns 403 without issuing a JWT. Local logins are unaffected. Setting INITIAL_PASSWORD also bypasses this check.

Known trade-off: this intentionally leaves no remote self-service password-change path — the change-password flow (PATCH /api/settings) requires a JWT, which we deliberately withhold. A remote fresh-install user must either change the password from the local machine or set INITIAL_PASSWORD before first launch. This is a deliberate security trade-off, not an oversight.

3. /api/usage/request-details returns full conversation content

The endpoint returned complete request (user prompts, tool calls), providerRequest, providerResponse, and response payloads for every stored request. Any dashboard-authenticated user (or anyone if requireLogin is disabled) could read all conversation history.

Fix: The four payload fields are replaced with { redacted: true }. Metadata (model, tokens, latency, status, timestamps) is preserved.

Build fix: declare chalk and prop-types

Both are directly imported by source files (src/lib/oauth/utils/ui.js imports chalk; dashboard provider pages import prop-types) but were never declared in package.json, causing npm run build to fail on fresh clones. Added as proper dependencies.

Test plan

  • 8 unit tests for resolveBaseUrl() SSRF guard (tests/unit/search-ssrf-guard.test.js): public http/https allowed; loopback, private IPs (10.x, 192.168.x, 172.16.x), localhost hostname, cloud metadata (169.254.169.254), and non-http protocols (file, gopher, ftp) all rejected.
  • 3 unit tests for request-details redaction (tests/unit/request-details-redaction.test.js): payloads redacted, metadata preserved, empty/null details handled.
  • E2E verified on Pi 500 + Docker v0.5.50 fresh deployment: SSRF attempt → 400 "Blocked URL: private IP", listener zero hits; public baseUrl passes through. Remote default-password login → 403 no cookie; local login unaffected.
  • npm run build passes with the new dependency declarations.

Known limitations

  • Bare next start / pm2 deployments (no custom-server.js): isLocalRequest() falls back to Host-header judgment, which is spoofable. This is a pre-existing issue (CVE-2026-56681 class) not introduced by this PR. Docker deployments (the recommended path, using custom-server.js) are not affected.
  • assertPublicUrl() validates hostnames but does not perform DNS resolution, so DNS-rebinding TOCTOU is out of scope — consistent with the existing /v1/fetch guard.

Supersedes #3050 and #3060.

…nticated npm install RCE

The /api/pxpipe/* routes (install, start, restart, stop) were not
listed in LOCAL_ONLY_PATHS, unlike their /api/headroom counterparts.
When requireLogin was false (fresh install default), a remote attacker
could POST to /api/pxpipe/install to trigger spawn('npm', ['install',
'pxpipe-proxy@latest']), achieving unauthenticated remote code execution
via supply-chain attack.

E2E verified: dev server with requireLogin=false, Host:evil.com → 403
after fix; /api/headroom already protected (no regression).
afandiaziz pushed a commit to afandiaziz/9router that referenced this pull request Aug 8, 2026
Cherry-picked from open upstream PRs (none merged upstream as of 2026-08-09):
  decolua#3078 /api/pxpipe -> LOCAL_ONLY_PATHS (defense in depth)
  decolua#3085 enforce requireApiKey on GET /v1/models
  decolua#3063 SSRF guard on search baseUrl + block default-password remote login
  decolua#3081 inject stream_options.include_usage for OpenAI-compatible upstreams
  decolua#3083 read cached_tokens from nested prompt_tokens_details

Verified: no test regressions vs v0.5.50 baseline (88 pre-existing
failures unchanged); +21 new passing tests.
golamrabbi696 added a commit to golamrabbi696/EzRouter that referenced this pull request Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant