The Signing Key Is in the Repository: CVE-2026-105147 Lets Anyone Mint an R2R Admin Token

Two CVE records for SciPhi-AI R2R — an agentic retrieval-augmented-generation server with 8,013 stars and a RESTful API — were published by VulDB on 4 October 2026. CVE-2026-105147 (11:15 UTC) is hardcoded credentials in the JWT secret handler. CVE-2026-105148 (11:30 UTC) is server-side request forgery via generation_config.api_base on the retrieval completion endpoint. Both say “up to 3.6.6”, both carry exploit-maturity proof-of-concept, and both end with the same sentence: “The vendor was contacted early about this disclosure but did not respond in any way.”

We pulled both CVE records, both referenced researcher advisories, and the current R2R source. The first bug is an authentication bypass that works against a default docker compose install, and the CVE record does not say so.

One string, two providers, every default install

R2R signs session JWTs with HS256 through a pluggable crypto provider. The default is bcrypt (provider = "bcrypt" in the shipped r2r.toml). In py/core/providers/crypto/bcrypt.py, at module scope:

DEFAULT_BCRYPT_SECRET_KEY = "wNFbczH3QhUVcPALwtWZCPi0lrDlGV3P1DPRVEQCPbM"  # Replace or load from env or secrets manager

and in the provider constructor:

self.secret_key = (
    config.secret_key
    or os.getenv("R2R_SECRET_KEY")
    or DEFAULT_BCRYPT_SECRET_KEY
)

The alternative provider, py/core/providers/crypto/nacl.py, defines DEFAULT_NACL_SECRET_KEY as the same literal string with the same fallback chain. Both then call jwt.encode(to_encode, self.secret_key, algorithm="HS256") and jwt.decode(token, self.secret_key, algorithms=["HS256"]). A symmetric algorithm means the key that verifies a token is the key that mints one — and that key is a constant in a public repository.

The fallback only matters if the environment variable is empty. R2R’s own docker/env/r2r.env and docker/env/r2r-full.env both ship the line R2R_SECRET_KEY=. An empty string is falsy in Python, so or walks past it to the constant. The shipped configuration selects the published key.

Three more defaults turn that into account takeover rather than a hygiene finding. The config sets require_authentication = false and default_admin_email = "admin@example.com", and R2RAuthProvider creates that superuser at startup. The researcher’s advisory describes forging an access token with sub=admin@example.com — the verifier requires only sub, token_type and exp — and reports GET /v3/users/me returning HTTP 200 with is_superuser=true against sciphiai/r2r:latest, with a negative control signed by a different secret returning 401.

Two consequences defenders usually get wrong on this bug class. Turning on require_authentication does not help if the key is still the default; the forged token authenticates. And rotating the admin password does not help either, because the signing key is independent of the password hash — previously forged tokens stay valid until R2R_SECRET_KEY itself changes.

The second record: the model prefix picks the HTTP client

CVE-2026-105148 is the quieter of the two and the more agent-specific. GenerationConfig in py/shared/abstractions/llm.py exposes an api_base field on the wire, and POST /v3/retrieval/completion accepts that object straight from the request body. What happens next is decided by the model string the caller chose. R2RCompletionProvider routes by prefix: openai/, azure/, deepseek/, ollama/, lmstudio/, anthropic/, azure-foundry/, or an unprefixed name go to the OpenAI provider, which ignores api_base. Anything else falls through to the LiteLLM provider, which passes api_base into the outbound call.

So the client selects the code path that honours its own URL, simply by naming a model with an unfamiliar prefix. On a default install, with require_authentication = false, the caller never logs in. The researcher reports the listener receiving a POST /v1/chat/completions with a litellm/… user agent — and, for the prefixes LiteLLM treats as OpenAI-compatible, an Authorization: Bearer header carrying the server’s OPENAI_API_KEY. With a huggingface/ prefix the same fetch fires without the header. The advisory is careful about that distinction, and so are we: whether the key leaves with the request depends on which provider LiteLLM resolves.

This is the recurring shape in agent middleware. A field that looks like configuration is reachable as input, and the sink is an HTTP client holding credentials. It is the same seam as the unguarded fetch tool in MCP’s own reference server and the SSRF in AWS’s Loom control plane: outbound requests made on behalf of an untrusted caller, from inside your network, with your secrets attached.

The scores do not describe the first bug

VulDB assigned both records the same numbers: CVSS 4.0 base 6.9 (MEDIUM) and CVSS 3.1 base 7.3 (HIGH), with the 3.1 vector AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L. Low confidentiality, low integrity, low availability.

For an unauthenticated superuser-token forgery on a document store, C:L/I:L is not a defensible reading. The researcher’s advisory for the JWT issue self-scores CVSS 3.1 9.8 (C:H/I:H/A:H) and CVSS 4.0 9.3, and classifies it CWE-321, CWE-798 and CWE-287 — improper authentication included. The CVE record keeps only the hardcoded-credential weaknesses (CWE-798, CWE-259) and drops the authentication consequence entirely. A triage queue sorted by CVSS puts a full auth bypass below a lot of noise.

We have written this paragraph before, about five mcp-remote CVEs whose descriptions diverged from the advisories they cited, and about MCP SDK advisories that npm audit never surfaced. The direction of the error differs each time — overstated there, understated here — and the operational lesson is identical: in AI tooling, the identifier is an index, not an assessment. Open the linked advisory.

There is no patch, and the shape of the project says why

Checked directly against the registries rather than inferred: the latest R2R release on PyPI is 3.6.6, uploaded 17 August 2025. The most recent commit on main is 9c5a94d, 7 November 2025 — which is also the revision the researcher reports testing against. The repository is not archived, carries 8,013 stars and 650 forks, and its GitHub security-advisories endpoint returns an empty list. The constants are still in main today; we read them there.

That is eleven months without a release, five without a commit, a live-looking repository, and a CVE whose own text records no vendor response. The pattern is familiar from MindSearch’s unauthenticated exec() and LightLLM’s ten unfixed CVEs. Popular AI infrastructure is accumulating network-listening code with no one on the other end of a report, and star counts keep being mistaken for maintenance.

What to do

  • Set R2R_SECRET_KEY to a unique value today, then rotate it. This is a one-line mitigation that does not require upstream. Generate at least 32 bytes of entropy. Rotating invalidates every token minted with the published key — including any an attacker already holds, which password changes would not.
  • Assume compromise if the API was ever network-reachable with the default key. Forged tokens leave no failed-login trail; they are valid signatures. Look for /v3/users/me and document-read traffic from unexpected sources, and treat ingested documents, prompts and collections as potentially read.
  • Turn on authentication and change the key. require_authentication = true with a default secret is not a control. Change default_admin_email away from admin@example.com too; the forgery needs a valid subject, and the shipped default supplies one.
  • Put egress policy around the completion path. For CVE-2026-105148, deny the R2R process outbound access to loopback, RFC1918 and 169.254.169.254, and allowlist your model providers. If you can, restrict accepted model prefixes to the ones that route to the OpenAI provider, which ignores api_base.
  • Rotate the provider keys in that container. OPENAI_API_KEY and its siblings are exactly what the SSRF path can carry outbound. Rotate first, investigate second.
  • Grep your own stack for the general defect. A fallback chain ending in a literal secret, and a request-supplied base URL reaching an authenticated HTTP client, are both findable with a search. Neither needs a CVE to be worth fixing.

Verification note: we read the CVE 5.2 records for CVE-2026-105147 and CVE-2026-105148 from the CVE Program API (both PUBLISHED 4 October 2026, assigner VulDB, advisory-disclosed 3 October 2026), the two researcher advisories they reference, and the current SciPhi-AI/R2R source on main — where we independently confirmed the two DEFAULT_*_SECRET_KEY constants and their identical value, the or fallback chain in both crypto providers, the HS256 encode/decode calls, R2R_SECRET_KEY= in both shipped Docker env files, require_authentication = false and default_admin_email in r2r.toml, the api_base field on GenerationConfig, and the prefix-based provider routing. Release dates come from the PyPI JSON API and commit metadata from the GitHub API. We did not deploy R2R, send requests to any instance, or attempt exploitation; the HTTP 200 superuser result, the 401 negative control and the outbound-header observations are the advisory author’s. The 9.8 and 9.3 figures are the researcher’s self-assessment, not NVD’s.

Sources: