Security

HarnessRouter: cross-org connection hijack via a made-up bearer

Service-only routes that write an organization's provider connections could be reached through a multi-tenant console with a made-up bearer (GHSA-p6cq-54cg-8mpv)

PUBLISHED SEVERITY CRITICALSTATUS PatchedGHSA-p6cq-54cg-8mpv
Patched

Fixed in HarnessRouter 0.31.8 (2026-10-06) by maintainer PR #418 (commit 28609ec): internal-only routes now refuse any request that carries an Authorization header, and the console no longer forwards /internal/ routes outside self-hosted mode. Track the fix at HarnessRouter/harnessrouter#418.

Product
HarnessRouter Community Edition (Docker image harnessrouter/harnessrouter) in HarnessRouter/harnessrouter
Affected versions
HarnessRouter before 0.31.8, when one deployment serves more than one organization behind a console that is not in self-hosted mode. A default self-hosted install is not affected. Fixed in 0.31.8 (2026-10-06).
Severity
CRITICAL
Status
Patched
Weaknesses
  • CWE-862Missing Authorization
  • CWE-863Incorrect Authorization
As filed on the advisory.
GitHub advisory
GHSA-p6cq-54cg-8mpv
Published by the maintainer on 2026-10-06
CVE
Pending (requested from GitHub)
Published
Credit
Finder: Aaron Elijah Mars of Aeon. Tool: Aeon (https://www.aeon.fun).

What users should do now

  1. If you run HarnessRouter for more than one organization behind a console that is not in self-hosted mode, upgrade to 0.31.8 or later (for example the harnessrouter/harnessrouter:0.31.8 image or a newer tag).
  2. Until you can upgrade, do not expose the console's /api/harness/internal/* paths or the /v1/orgs/*/connections/* and /v1/orgs/*/policy/* paths to the outside. This is the maintainer's workaround.
  3. After upgrading, check every organization's provider connections and routing policy for a base_url or provider you did not set, and fix any you find.
  4. A default self-hosted install needs no action for this issue: its proxy gives the key to a signed-in session only and pins the organization on the server.

Summary

HarnessRouter puts one API in front of agent harnesses such as Claude Code, Codex and Pi. Its gateway has a set of routes meant only for internal services that hold the server's internal key: reading and writing an organization's provider connections and routing policy, and the /internal/* routes. The maintainer counts thirteen of them. Before 0.31.8 these routes asked for the key and nothing else, and took the organization from the URL. The console's proxy, when it is not in self-hosted mode, added that key to every request that carried an Authorization header, before anything checked the header. So through such a console, a request with a made-up bearer reached these routes. With PUT /v1/orgs/<another organization>/connections/<name> a caller could point that organization's model connection at an endpoint of their own choosing, so that the organization's next tasks sent their prompts there and acted on what came back.

Affected versions

HarnessRouter (HarnessRouter/harnessrouter) before 0.31.8, when one deployment serves more than one organization behind a console that is not in self-hosted mode. Fixed in 0.31.8 (2026-10-06). A default self-hosted install is not affected: there the proxy adds the key only for a valid signed-in session and pins the organization and member from server constants.

Impact

  • Cross-organization write of provider connections. PUT /v1/orgs/{org}/connections/{name} stores a connection record, including provider, base_url and credentials, for whatever organization is named in the URL. A caller with no valid sign-in could replace another organization's connection, so that organization's later tasks sent their prompts to an endpoint the caller controls and acted on the replies.
  • Cross-organization write of routing policy. PUT /v1/orgs/{org}/policy/{backend} sets which connections an organization's backend uses, for any organization.
  • **Read of connection metadata and the /internal/* routes.** GET /v1/orgs/{org}/connections/{name} and the internal routes (storage usage, session recycle, trace reindex, workspace backfill, plug attachments, pricing and tools) were reachable the same way.
  • No valid account is needed: the bearer only has to be present, not valid. The advisory scores it CVSS 3.1 9.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N).

The maintainer's fix notes that the same flaw was found live on the hosted service and closed there the same way on 2026-10-06 (code comment).

The report had a second finding: a console session's workspace is the browser's own claim and was not checked on routes that take a session id. The maintainer settled it by design and by 0.31.7 (GHSA-9pjr-97x3-c4wv): a person signed in to the console has organization reach on purpose, and the boundary between workspaces is the API key, enforced on every route that takes an id since 0.31.7.

Affected code

Permalinks at 249c6cc (0.31.7, the last release before the fix).

Fix

  • #418 (merged as 28609ec, released in 0.31.8) makes _internal_only refuse any request that also carries an Authorization header. A service calls with the internal key and nothing else; a bearer means a person or an API key, relayed. This closes all thirteen routes at once.
  • The same change compares the internal key in constant time (hmac.compare_digest).
  • The console proxy now returns 404 for /internal/* paths when it is not in self-hosted mode, so it no longer forwards them at all.
  • The PR adds gateway/tests/test_internal_only.py, including a test that fails if an /internal/ route or one of the three connection and policy routes is not behind the guard.
  • Verified at main (79e0dbf, 2026-10-09): the bearer refusal and the console's /internal/ block are both still in place.

Detection (for defenders)

In console and gateway logs, look for PUT or GET requests to /v1/orgs/{org}/connections/*, PUT requests to /v1/orgs/{org}/policy/*, and any /internal/* request that came in through the console with an Authorization header, especially where the organization in the URL is not the caller's own. In stored data, look for connection records whose base_url or provider nobody on that organization set, and for tasks whose model traffic went to an unfamiliar host. A working exploit is withheld.

Timeline

  1. Reported privately via GitHub PVR (GHSA-p6cq-54cg-8mpv).
  2. HarnessRouter 0.31.7 released, holding API keys to their own workspace (the maintainer's answer to the report's second finding).
  3. Maintainer merges #418, releases 0.31.8 with the fix, and publishes the advisory as CRITICAL.
  4. CVE request issue #441 opened; the maintainer requests a CVE from GitHub and closes the issue.

Credit

Finder: Aaron Elijah Mars of Aeon. Tool: Aeon.

References