Merge branch 'release-1.10.0' into chore/deployments-tech-debt-review

This commit is contained in:
Viktor Avelino
2026-04-22 10:11:01 -04:00
committed by GitHub
4 changed files with 116 additions and 21 deletions

View File

@ -1701,16 +1701,6 @@
"is_secret": false
}
],
"src/backend/base/langflow/api/utils/core.py": [
{
"type": "Secret Keyword",
"filename": "src/backend/base/langflow/api/utils/core.py",
"hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4",
"is_verified": false,
"line_number": 413,
"is_secret": false
}
],
"src/backend/base/langflow/initial_setup/starter_projects/Basic Prompt Chaining.json": [
{
"type": "Hex High Entropy String",
@ -8373,5 +8363,5 @@
}
]
},
"generated_at": "2026-04-15T15:25:36Z"
"generated_at": "2026-04-21T23:47:54Z"
}

View File

@ -398,19 +398,44 @@ def custom_params(
return Params(page=page or MIN_PAGE_SIZE, size=size or MAX_PAGE_SIZE)
def extract_global_variables_from_headers(headers) -> dict[str, str]:
"""Extract global variables from HTTP headers with prefix X-LANGFLOW-GLOBAL-VAR-*.
# Well-known authentication headers that can be propagated to nested MCP calls
# when ``include_auth_headers=True`` is passed. These are stored under their
# lowercase header names so that nested server configs can reference them
# directly, e.g. ``{"x-api-key": "x-api-key"}`` in the MCP server headers config.
_AUTH_HEADERS_TO_PROPAGATE = frozenset({"x-api-key", "authorization"})
def extract_global_variables_from_headers(headers, *, include_auth_headers: bool = False) -> dict[str, str]:
"""Extract global variables from HTTP headers.
By default, only headers with the ``X-LANGFLOW-GLOBAL-VAR-*`` prefix are
extracted. When ``include_auth_headers=True``, the well-known authentication
headers ``x-api-key`` and ``authorization`` are additionally captured under
their lowercase names so that nested MCP server configs can reference them
directly (e.g. ``{"x-api-key": "x-api-key"}``).
SECURITY NOTE: Only pass ``include_auth_headers=True`` from MCP call sites
(see ``api/v1/mcp_projects.py``). On non-MCP routes such as ``/run`` and
``/workflow``, ``x-api-key`` is Langflow's own authentication key — exposing
it in ``request_variables`` would make it readable by any component that
reads the graph context.
Args:
headers: HTTP headers object (e.g., from FastAPI Request.headers)
headers: HTTP headers object (e.g., from FastAPI Request.headers).
include_auth_headers: When True, also extract well-known authentication
headers (``x-api-key``, ``authorization``) under their lowercase
names. Should only be set by MCP request handlers that need to
propagate these values to nested MCP calls.
Returns:
Dictionary mapping variable names (uppercase) to their values
Dictionary mapping variable names to their values.
Example:
headers = {"X-LANGFLOW-GLOBAL-VAR-API-KEY": "secret", "Content-Type": "application/json"}
result = extract_global_variables_from_headers(headers)
# Returns: {"API_KEY": "secret"}
headers = {"X-LANGFLOW-GLOBAL-VAR-API-KEY": "secret", "x-api-key": "mykey"}
extract_global_variables_from_headers(headers)
# Returns: {"API-KEY": "secret"}
extract_global_variables_from_headers(headers, include_auth_headers=True)
# Returns: {"API-KEY": "secret", "x-api-key": "mykey"}
"""
variables: dict[str, str] = {}
@ -420,6 +445,8 @@ def extract_global_variables_from_headers(headers) -> dict[str, str]:
if header_lower.startswith(LANGFLOW_GLOBAL_VAR_HEADER_PREFIX):
var_name = header_lower[len(LANGFLOW_GLOBAL_VAR_HEADER_PREFIX) :].upper()
variables[var_name] = header_value
elif include_auth_headers and header_lower in _AUTH_HEADERS_TO_PROPAGATE:
variables[header_lower] = header_value
except Exception as exc: # noqa: BLE001
# Log the error but don't raise - we want to continue execution
logger.exception("Failed to extract global variables from headers: %s", exc)

View File

@ -345,7 +345,7 @@ async def handle_project_sse(
user_token = current_user_ctx.set(current_user)
project_token = current_project_ctx.set(project_id)
variables = extract_global_variables_from_headers(request.headers)
variables = extract_global_variables_from_headers(request.headers, include_auth_headers=True)
req_vars_token = current_request_variables_ctx.set(variables or None)
try:
@ -386,7 +386,7 @@ async def _handle_project_sse_messages(
"""Handle POST messages for a project-specific MCP server using SSE transport."""
user_token = current_user_ctx.set(current_user)
project_token = current_project_ctx.set(project_id)
variables = extract_global_variables_from_headers(request.headers)
variables = extract_global_variables_from_headers(request.headers, include_auth_headers=True)
req_vars_token = current_request_variables_ctx.set(variables or None)
try:
@ -443,7 +443,7 @@ async def _dispatch_project_streamable_http(
user_token = current_user_ctx.set(current_user)
project_token = current_project_ctx.set(project_id)
variables = extract_global_variables_from_headers(request.headers)
variables = extract_global_variables_from_headers(request.headers, include_auth_headers=True)
request_vars_token = current_request_variables_ctx.set(variables or None)
try:

View File

@ -1,6 +1,7 @@
from types import SimpleNamespace
import pytest
from langflow.api.utils.core import extract_global_variables_from_headers
from langflow.api.v1 import mcp_utils
from lfx.interface.components import component_cache
@ -122,3 +123,80 @@ async def test_handle_list_tools_skips_blocked_custom_flows(monkeypatch):
tools = await mcp_utils.handle_list_tools()
assert tools == []
class TestExtractGlobalVariablesFromHeaders:
"""Unit tests for ``extract_global_variables_from_headers``.
Covers the MCP auth-header propagation fix (issue #12529): ``x-api-key``
and ``authorization`` should be captured under their lowercase names when
(and only when) ``include_auth_headers=True`` is passed. The default
behavior must remain backwards-compatible for non-MCP routes, where
``x-api-key`` is Langflow's own auth key and must not leak into the graph
context.
"""
def test_langflow_global_var_prefix_still_extracted(self):
"""Regression guard: ``X-LANGFLOW-GLOBAL-VAR-*`` extraction is preserved."""
headers = {
"X-LANGFLOW-GLOBAL-VAR-API-KEY": "secret-value",
"X-LANGFLOW-GLOBAL-VAR-DB-URL": "postgres://host/db",
"Content-Type": "application/json",
}
result = extract_global_variables_from_headers(headers)
assert result == {"API-KEY": "secret-value", "DB-URL": "postgres://host/db"}
def test_auth_headers_not_extracted_by_default(self):
"""Non-MCP call sites: ``x-api-key`` / ``authorization`` must not leak through."""
headers = {
"x-api-key": "langflow-auth-key",
"authorization": "Bearer token",
"X-LANGFLOW-GLOBAL-VAR-MY-VAR": "value",
}
result = extract_global_variables_from_headers(headers)
assert "x-api-key" not in result
assert "authorization" not in result
assert result == {"MY-VAR": "value"}
def test_auth_headers_extracted_under_lowercase_when_opted_in(self):
"""MCP call sites: lowercase auth headers are captured when opted in."""
headers = {
"x-api-key": "api-key-value",
"authorization": "Bearer jwt-token",
}
result = extract_global_variables_from_headers(headers, include_auth_headers=True)
assert result == {"x-api-key": "api-key-value", "authorization": "Bearer jwt-token"}
def test_auth_header_matching_is_case_insensitive(self):
"""Headers with mixed or uppercase casing still match (e.g. ``X-Api-Key``, ``AUTHORIZATION``)."""
headers = {
"X-Api-Key": "mixed-case-value",
"AUTHORIZATION": "Bearer UPPER",
}
result = extract_global_variables_from_headers(headers, include_auth_headers=True)
assert result == {"x-api-key": "mixed-case-value", "authorization": "Bearer UPPER"}
def test_both_categories_extracted_together(self):
"""``X-LANGFLOW-GLOBAL-VAR-*`` and auth headers coexist when opted in."""
headers = {
"X-LANGFLOW-GLOBAL-VAR-API-KEY": "global-secret",
"x-api-key": "incoming-mcp-key",
"Authorization": "Bearer mcp-token",
"Content-Type": "application/json",
}
result = extract_global_variables_from_headers(headers, include_auth_headers=True)
assert result == {
"API-KEY": "global-secret",
"x-api-key": "incoming-mcp-key",
"authorization": "Bearer mcp-token",
}