Skip to content

🔐 feat: Resolve allowedAddresses Entries From Server Environment Variables - #16852

Open
bensi94 wants to merge 2 commits into
LibreChat-AI:devfrom
aproorg:feat/allowed-addresses-env
Open

bensi94 wants to merge 2 commits into
LibreChat-AI:devfrom
aproorg:feat/allowed-addresses-env

Conversation

@bensi94

@bensi94 bensi94 commented Oct 7, 2026 •

Copy link
Copy Markdown

Summary

allowedAddresses entries can't come from the environment, so a librechat.yaml shared 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 as searxng.<namespace>.local:8080. Since web search and scrape egress moved behind the SSRF-safe agent (#14606), each of those hosts needs a webSearch.allowedAddresses entry, or every request fails with SSRF 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 ordinary host:port entry 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 whole host:port entry, and ${VAR}:port, where it holds only the host. The config loader resolves them once at load time, for every allowedAddresses list (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 as searxng.${NS}:8080 or ${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 configSchema validation and before the config reaches AppService. Each allowedAddresses list it finds in the loaded file is rewritten in place to plain host:port strings, so nothing on the request path changes.

createCustomConfigLoader            packages/api/src/app/loader.ts
  configSchema.safeParse            accepts ${VAR} and ${VAR}:port, rejects other $ { }
  resolveConfigAllowedAddresses     packages/api/src/app/addresses.ts, walks every allowedAddresses list
    resolveAllowedAddressesEnv      packages/api/src/auth/allowedAddresses.ts
      parseAllowedAddressEnvReference   packages/data-provider/src/config.ts
      isSensitiveEnvVar             drop, never read
      env[VAR] unset/empty          drop ('unset')
      normalizeAddressEntry(value)  drop ('invalid') unless it passes the existing entry rules
  logger.warn per drop              names the path, entry and reason, never the value

These guarantees are what make this safe as an SSRF exemption source:

  • Only the server environment is read, and only for the YAML file the loader just read. DB config overrides, user-provided endpoint or web-search URLs and programmatic lists never go through resolution.
  • normalizeAddressEntry now 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.
  • The resolved value must pass the existing rules: host:port with 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.
  • Variables on the existing isSensitiveEnvVar denylist (MONGO_URI, JWT_SECRET, CREDS_KEY, …) are never read.
  • The warning omits the value, because a misconfigured variable may hold a URL with credentials:
[allowedAddresses] Dropped webSearch.allowedAddresses entry ${SEARXNG_ALLOWED_ADDRESS}: the environment variable is unset or empty

Example:

webSearch:
  allowedAddresses:
    - '${SEARXNG_ALLOWED_ADDRESS}'   # SEARXNG_ALLOWED_ADDRESS=searxng.search-ns.local:8080
    - '${FIRECRAWL_HOST}:3002'       # FIRECRAWL_HOST=firecrawl.search-ns.local

Type of change

  • Feature
  • Documentation

Testing

  1. Put the YAML above in librechat.yaml, set SEARXNG_ALLOWED_ADDRESS=searxng.search-ns.local:8080 and FIRECRAWL_HOST=firecrawl.search-ns.local, and start LibreChat. The loaded webSearch.allowedAddresses is ["searxng.search-ns.local:8080", "firecrawl.search-ns.local:3002"].
  2. Unset both variables and restart. Startup continues, the two entries are dropped and each one logs a warning. Other lists are unchanged.
  3. Change an entry to 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}:port literal, and parseAllowedAddressEnvReference.
  • 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 real createCustomConfigLoader on YAML. It covers resolution and matching, startup without the variables (no process.exit, warnings logged, other lists intact), invalid values being dropped without being logged, partial references failing validation, and every allowedAddresses field 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 --noEmit in packages/data-provider and packages/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

  • Config schema: the change is additive for valid configs. An entry containing $, { 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.
  • Admin overrides: the shared schema means an admin override can store a reference, but it is never resolved and the runtime parser drops it.
  • Performance: resolution adds one in-memory pass over the loaded YAML at startup and on reload. There are no DB reads and no request-path cost.
  • librechat.example.yaml documents the syntax in the webSearch block.

…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.
@codegraph-librechat codegraph-librechat Bot added 🗺️ Backend Infra codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🗺️ Platform Security codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) labels Oct 9, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🗺️ Backend Infra codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9) 🗺️ Platform Security codegraph: the taxonomy area this belongs to (classifier, confidence ≥ 0.9)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant