mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-17 13:16:34 +01:00
* 🔒 fix: Resolve env vars before body placeholder expansion to prevent secret exfiltration Body placeholders ({{LIBRECHAT_BODY_*}}) were substituted before extractEnvVariable ran, allowing user-controlled body fields containing ${SECRET} patterns to be expanded into real environment values in outbound headers. Reorder so env vars resolve first, preventing untrusted input from triggering env expansion. * 🛡️ fix: Block sensitive infrastructure env vars from placeholder resolution Add isSensitiveEnvVar blocklist to extractEnvVariable so that internal infrastructure secrets (JWT_SECRET, JWT_REFRESH_SECRET, CREDS_KEY, CREDS_IV, MEILI_MASTER_KEY, MONGO_URI, REDIS_URI, REDIS_PASSWORD) can never be resolved via ${VAR} expansion — even if an attacker manages to inject a placeholder pattern. Uses exact-match set (not substring patterns) to avoid breaking legitimate operator config that references OAuth/API secrets in MCP and custom endpoint configurations. * 🧹 test: Rename ANOTHER_SECRET test fixture to ANOTHER_VALUE Avoid using SECRET-containing names for non-sensitive test fixtures to prevent confusion with the new isSensitiveEnvVar blocklist. * 🔒 fix: Resolve env vars before all user-controlled substitutions in processSingleValue Move extractEnvVariable to run on the raw admin-authored template BEFORE customUserVars, user fields, OIDC tokens, and body placeholders. Previously env resolution ran after customUserVars, so a user setting a custom MCP variable to "${SECRET}" could still trigger env expansion. Now env vars are resolved strictly on operator config, and all subsequent user-controlled substitutions cannot introduce ${VAR} patterns that would be expanded. Gated by !dbSourced so DB-stored servers continue to skip env resolution. Adds a security-invariant comment documenting the ordering requirement. * 🧪 test: Comprehensive security regression tests for placeholder injection - Cover all three body fields (conversationId, parentMessageId, messageId) - Add user-field injection test (user.name containing ${VAR}) - Add customUserVars injection test (MY_TOKEN = "${VAR}") - Add processMCPEnv injection tests for body and customUserVars paths - Remove redundant process.env setup/teardown already handled by beforeEach/afterEach * 🧹 chore: Add REDIS_PASSWORD to blocklist integration test; document customUserVars gate
85 lines
2.3 KiB
TypeScript
85 lines
2.3 KiB
TypeScript
export const envVarRegex = /^\${(.+)}$/;
|
|
|
|
/**
|
|
* Infrastructure env vars that must never be resolved via placeholder expansion.
|
|
* These are internal secrets whose exposure would compromise the system —
|
|
* they have no legitimate reason to appear in outbound headers, MCP env/args, or OAuth config.
|
|
*
|
|
* Intentionally excludes API keys (operators reference them in config) and
|
|
* OAuth/session secrets (referenced in MCP OAuth config via processMCPEnv).
|
|
*/
|
|
const SENSITIVE_ENV_VARS = new Set([
|
|
'JWT_SECRET',
|
|
'JWT_REFRESH_SECRET',
|
|
'CREDS_KEY',
|
|
'CREDS_IV',
|
|
'MEILI_MASTER_KEY',
|
|
'MONGO_URI',
|
|
'REDIS_URI',
|
|
'REDIS_PASSWORD',
|
|
]);
|
|
|
|
/** Returns true when `varName` refers to an infrastructure secret that must not leak. */
|
|
export function isSensitiveEnvVar(varName: string): boolean {
|
|
return SENSITIVE_ENV_VARS.has(varName);
|
|
}
|
|
|
|
/** Extracts the environment variable name from a template literal string */
|
|
export function extractVariableName(value: string): string | null {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
const match = value.trim().match(envVarRegex);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
/** Extracts the value of an environment variable from a string. */
|
|
export function extractEnvVariable(value: string) {
|
|
if (!value) {
|
|
return value;
|
|
}
|
|
|
|
const trimmed = value.trim();
|
|
|
|
const singleMatch = trimmed.match(envVarRegex);
|
|
if (singleMatch) {
|
|
const varName = singleMatch[1];
|
|
if (isSensitiveEnvVar(varName)) {
|
|
return trimmed;
|
|
}
|
|
return process.env[varName] || trimmed;
|
|
}
|
|
|
|
const regex = /\${([^}]+)}/g;
|
|
let result = trimmed;
|
|
|
|
const matches = [];
|
|
let match;
|
|
while ((match = regex.exec(trimmed)) !== null) {
|
|
matches.push({
|
|
fullMatch: match[0],
|
|
varName: match[1],
|
|
index: match.index,
|
|
});
|
|
}
|
|
|
|
for (let i = matches.length - 1; i >= 0; i--) {
|
|
const { fullMatch, varName, index } = matches[i];
|
|
if (isSensitiveEnvVar(varName)) {
|
|
continue;
|
|
}
|
|
const envValue = process.env[varName] || fullMatch;
|
|
result = result.substring(0, index) + envValue + result.substring(index + fullMatch.length);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Normalize the endpoint name to system-expected value.
|
|
* @param name
|
|
*/
|
|
export function normalizeEndpointName(name = ''): string {
|
|
return name.toLowerCase() === 'ollama' ? 'ollama' : name;
|
|
}
|