Skip to content
Merged
14 changes: 2 additions & 12 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -2968,7 +2958,7 @@
"filename": "src/backend/tests/unit/api/v1/test_mcp_projects.py",
"hashed_secret": "4258d43e3b1f9658067ceea9c682a96cbdbb5ca0",
"is_verified": false,
"line_number": 596,
"line_number": 683,
"is_secret": false
}
],
Expand Down Expand Up @@ -8373,5 +8363,5 @@
}
]
},
"generated_at": "2026-04-15T15:25:36Z"
"generated_at": "2026-04-21T23:22:42Z"
}
41 changes: 34 additions & 7 deletions src/backend/base/langflow/api/utils/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {}

Expand All @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions src/backend/base/langflow/api/v1/mcp_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
87 changes: 87 additions & 0 deletions src/backend/tests/unit/api/v1/test_mcp_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,93 @@ def test_args_reference_urls_matches_non_last_string_argument():
assert _args_reference_urls(args, urls) is True


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."""
from langflow.api.utils.core import extract_global_variables_from_headers

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."""
from langflow.api.utils.core import extract_global_variables_from_headers

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."""
from langflow.api.utils.core import extract_global_variables_from_headers

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``)."""
from langflow.api.utils.core import extract_global_variables_from_headers

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."""
from langflow.api.utils.core import extract_global_variables_from_headers

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",
}


@pytest.fixture
def mock_project(active_user):
"""Fixture to provide a mock project linked to the active user."""
Expand Down
Loading