Repository navigation
Conversation
…igning
get_session_middleware_key() returned a deterministic string built from
public class and config names:
f'{self.__class__.__name__}___{self.config.name}'
Both components are knowable by any client (class name is in source and
error responses; config name is the public benchmark server name). This
string was then reused as the signing secret for:
1. Starlette SessionMiddleware (secret_key=) — signs session cookies
2. MCP auto-exposure URLSafeSerializer — signs per-rollout MCP tokens
Knowing the secret, any client can forge session cookies and MCP tokens
offline. A resources server holds session_state for ALL concurrent
rollouts in a single process, keyed by session_id; a forged cookie
carrying another rollout's session_id lets one rollout read or clobber
another's state. A forged MCP token with tools=None defeats the
per-rollout tool allow-list (mcp_allowed_tools_for_session).
The code's own comment acknowledged this was a placeholder ('override
in case we want to ever use an actual session middleware secret key'),
but no benchmark in the repo overrode it.
Fix: split the concerns. get_session_middleware_key() continues to
return the stable, human-readable identifier (used as the cookie name,
in error messages, and as a profiling-dir name — all non-security
contexts). A new get_session_secret_key() returns a cached per-process
random secret (secrets.token_hex(32)) used only as the signing key.
Override get_session_secret_key() for multi-process deployments where
all workers must agree on the key.
PoC (before fix): forge a session cookie offline with the predictable
secret; server accepts it as any session_id. After fix: the per-process
secret is unpredictable, so forgery fails.
This was referenced Sep 8, 2026
This was referenced Oct 22, 2026
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
get_session_middleware_key()returned a deterministic string built from two public components — the Python class name and the benchmark's config name:Both are knowable by any client: the class name appears in source and in error responses (
server_utils.py:567); the config name is the public benchmark server name. This string was reused as the signing secret for both:SessionMiddleware(server_utils.py:555,secret_key=session_middleware_key) — signs session cookies viaitsdangerous.URLSafeSerializer(mcp_auto_exposure.py:760-761,secret = server.get_session_middleware_key()) — signs per-rollout MCP session tokens.Knowing the secret, any client can forge session cookies and MCP tokens offline.
The code's own comment acknowledged this was a placeholder:
…but no benchmark in the repo overrides it, so the default ships with every example environment.
Impact
A resources server is a single process holding
session_statefor all concurrent rollouts (base_resources_server.py), keyed bysession_id. A forged cookie carrying another rollout'ssession_idlets one rollout read or clobber another's env state. A forged MCP token with"tools": Nonedefeats the per-rollout tool allow-list (mcp_allowed_tools_for_sessionatbase_resources_server.py:132). This breaks rollout isolation even on localhost — the threat isn't a remote attacker, it's one rollout's agent accessing another rollout's state within the same benchmark run.Honest scope: this does not grant remote code execution or network-level access. The integrity failure is within-process: cross-rollout
session_stateaccess and MCP tool-allow-list bypass. That's still a real trust-boundary failure in a multi-tenant RL eval framework.Reproduction
Verified locally: both forgeries succeed with the pre-fix secret and fail with a random per-process secret.
The fix
Split the concerns.
get_session_middleware_key()continues to return the stable, human-readable identifier (used as the cookie name, in error messages, and as a profiling-dir name — all non-security contexts). A newget_session_secret_key()returns a cached per-process random secret (secrets.token_hex(32)) used only as the signing key forSessionMiddlewareand the MCP token serializer:The caching is important:
get_session_secret_key()is called from bothsetup_session_middlewareandinstall_auto_exposure, and they must agree on the same secret. For multi-process deployments where all workers must share the key, overrideget_session_secret_key()to return a deployment-wide stable value (read from an env var or secret store).Verification
ruff checkpasses (line-length 119, full rule set).tests/unit_tests/test_server_utils.pyandtests/unit_tests/test_mcp_auto_exposure.pylocally due to missing optional deps in my environment (omegaconf,ray); CI will run them. The change is additive (new method + two call-site swaps) and does not alter any existing method's signature or return type.Checklist
get_session_middleware_key()unchanged (still used for cookie name, error messages, profiling dir)ruff checkpasses