Repository navigation
Conversation
…ables
allowedAddresses entries in librechat.yaml may now be ${VAR} (the variable
holds host:port) or ${VAR}:port (the variable holds the host). The config
loader resolves them once from the server environment; an unset, secret or
invalid variable drops only that entry with a warning, so startup continues.
The schema rejects any other placement of $, { or }, which previously passed
validation and was stored as a literal hostname that matched nothing. The
runtime parser drops any entry still carrying a reference, so DB overrides and
programmatic lists never gain environment resolution.
1 task done
This branch has not been deployed
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.
Summary
allowedAddressesentries can't come from the environment, so alibrechat.yamlshared across several deployments can't exempt hosts that differ per deployment. A common case is self-hosted SearXNG or Firecrawl reached through a per-environment service-discovery name such assearxng.<namespace>.local:8080. Since web search and scrape egress moved behind the SSRF-safe agent (#14606), each of those hosts needs awebSearch.allowedAddressesentry, or every request fails withSSRF protection: searxng.<namespace>.local resolved to blocked address 10.x.x.x. Operators already pass these hosts to the container as environment variables, but there was no way to reference one from the list.Writing a reference by hand also fails silently today.
"${SEARXNG_HOST}:8080"passes schema validation as an ordinaryhost:portentry and is stored as the literal hostname${searxng_host}, so it matches nothing and gives no warning.This PR lets an entry name a server environment variable in two shapes:
${VAR}, where the variable holds a wholehost:portentry, and${VAR}:port, where it holds only the host. The config loader resolves them once at load time, for everyallowedAddresseslist (endpoints,actions,mcpSettings,webSearch,ocr,speech.stt,speech.tts). If a variable is unset, empty, invalid or on the secret denylist, that one entry is dropped with a startup warning and the server keeps running, so a deployment without the optional service needs no separate config. Any other use of$,{or}in an entry, such assearxng.${NS}:8080or${HOST}:${PORT}, now fails schema validation instead of being stored as a hostname that never matches.Related to #16853
How it works
Resolution runs only in the YAML loader, after
configSchemavalidation and before the config reaches AppService. EachallowedAddresseslist it finds in the loaded file is rewritten in place to plainhost:portstrings, so nothing on the request path changes.These guarantees are what make this safe as an SSRF exemption source:
normalizeAddressEntrynow drops any entry containing$,{or}. A reference that reaches the runtime parser some other way, such as an admin override, grants nothing even when the variable is set.host:portwith a port, no URL, path, CIDR or whitespace, and no public IP literal. It is never resolved a second time, so${A}holding${B}is dropped.isSensitiveEnvVardenylist (MONGO_URI,JWT_SECRET,CREDS_KEY, …) are never read.Example:
Type of change
Testing
librechat.yaml, setSEARXNG_ALLOWED_ADDRESS=searxng.search-ns.local:8080andFIRECRAWL_HOST=firecrawl.search-ns.local, and start LibreChat. The loadedwebSearch.allowedAddressesis["searxng.search-ns.local:8080", "firecrawl.search-ns.local:3002"].searxng.${NS}:8080. Config validation fails at startup, just as it does for any other malformed entry.Tested environments/configuration: Node 25, macOS, unit and loader-level tests only. Not run against a live search deployment.
Automated tests:
packages/data-provider:npx jest src/config.spec.ts, 480 passed. New cases cover the two accepted shapes, the rejected partial and stray forms including the previously silent${VAR}:portliteral, andparseAllowedAddressEnvReference.packages/api:npx jest src/app src/auth src/web src/admin src/oauth src/endpoints src/mcp/oauth/handler.allowedAddresses.test.ts, 101 suites and 3434 tests passed. The new and changed specs are:auth/allowedAddresses.spec.ts: entries resolve and match only on the listed port. Unset, empty or blank variables are dropped. Values that are a URL, a bare host, CIDR, a public IP, contain whitespace, a nested reference or a bad port are dropped. Sensitive variables are never read. An unresolved${...}is dropped at runtime even when the variable is set. Literal entries are unchanged.app/addresses.spec.ts: runs the realcreateCustomConfigLoaderon YAML. It covers resolution and matching, startup without the variables (noprocess.exit, warnings logged, other lists intact), invalid values being dropped without being logged, partial references failing validation, and everyallowedAddressesfield being walked.auth/agent.spec.ts: the connect-time lookup with DNS mocked to a private address is allowed for the resolved entry and blocked for an unresolved one.api:npx jest server/services/Config/loadCustomConfig.spec.js, 28 passed.npx tsc --noEmitinpackages/data-providerandpackages/api: clean.npm run static-checks -- --against upstream/dev --full: all passed. depcheck was skipped because it isn't installed locally.Screenshots / recordings
No user-facing change.
Risk / compatibility
$,{or}that isn't one of the two reference shapes now fails validation. Such an entry could never match a real host before, so the only configs affected are ones that were already silently broken.librechat.example.yamldocuments the syntax in thewebSearchblock.