Fix: Dark Lab tool override silently drops parameters/inputSchema - #577
Open
Deez-Automations wants to merge 1 commit into
Conversation
_apply_tool_overrides only ever applied the "description" key from a
stored tool override; "parameters" (or "inputSchema") was accepted
and persisted by the API with no restriction, but silently discarded
at apply time -- the DB said the override was complete, the agent
runtime never actually saw the modified parameter schema.
The linked issue's own suggested fix (tool.inputSchema = new_parameters)
does not work: confirmed against the actual installed fastmcp package
that Tool is a pydantic model with extra="forbid", inputSchema is not
a declared field there (only on the wire-protocol MCPTool object built
by Tool.to_mcp_tool(), which reads self.parameters) -- setting it
raises ValueError. The correct attribute is tool.parameters.
Accepts "parameters" or "inputSchema" as the override key, applies
description and/or parameters independently, writes to the correct
attribute, verified end-to-end through to_mcp_tool() (what the LLM
actually receives), not just an internal field nobody reads.
Two rounds of review caught real gaps in the fix itself, both fixed
before this PR:
- A truthy-check (`a or b`) silently dropped a deliberate
`"parameters": {}` override (falsy in Python) -- the exact bug class
this issue was filed for, reproduced in miniature. Switched to
explicit key-presence checks.
- A non-dict "parameters" value, or a non-string "description" value,
or a tool's entire override entry being a non-object, doesn't fail
at assignment time (fastmcp doesn't validate on plain attribute
assignment) -- it fails later, in unrelated code that lists a
server's tools (or, for a non-object override entry, crashes server
creation entirely), on a future request with no connection to
whoever set the bad override. All three shapes are now rejected at
write time (a pydantic validator on the API request model) and,
defensively, at apply time too, rather than deferred to an
unrelated later crash.
Resolves GenAI-Security-Project#547
This was referenced Aug 20, 2026
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
The Dark Lab supply-chain tool override endpoint (
PUT /darklab/api/v1/supply-chain/servers/{server_type}/tools) accepts and persists a full override object per tool (any JSON keys, no restriction) intoMCPServerConfig.tool_overrides_json. But_apply_tool_overridesonly ever read thedescriptionkey from each override —parameters(orinputSchema) was accepted, stored, and reported back as applied, but silently discarded at apply time. The DB says the override is complete; the agent runtime never actually saw the modified parameter schema, so parameter-schema poisoning (a real, intended attack class — Dark Lab's whole purpose is being this platform's tool-poisoning sandbox, confirmed by its own docstring and by the two already-shipped Tool Poisoning Deletion/Exfil challenges using the same underlying mechanism for descriptions) was silently non-functional.A correction to the linked issue's own suggested fix
The issue proposes:
I verified this against the actual installed
fastmcppackage before writing anything, rather than trusting it:Toolis a pydantic model withmodel_config = {"extra": "forbid"}, andinputSchemais not a declared field on it — that name only exists on the wire-protocolMCPToolobject built byTool.to_mcp_tool(), which readsself.parametersinternally. Confirmed empirically:Applying the issue's diff verbatim would have crashed tool-override application with a misleading log message, after the description half had already silently applied. The correct attribute is
tool.parameters, confirmed end-to-end throughtool.to_mcp_tool().inputSchema— what the LLM actually receives, not just an internal field.Comparison against another open fix for the same issue
Another contributor has an independent, already-open PR (#549) for this same issue. Worth flagging directly since both are unmerged: #549's diff makes the identical
tool.inputSchema = new_parametersassignment as the linked issue's own suggested fix above — I re-ran the exact empirical check against that PR's diff and it hits the sameValueError: "FunctionTool" object has no field "inputSchema". Not a judgment call, just re-confirming the same runtime fact against a second piece of code that makes the same assumption. This PR usestool.parametersinstead, which is the field that actually exists on the model.Fix
_apply_tool_overridesnow appliesdescriptionand/orparametersindependently (acceptsparametersorinputSchemaas the input key, writes totool.parameters), andToolOverridesUpdate(the API request model) gained afield_validatorontool_overrides.Two review rounds, each catching a real gap in the fix itself
Round 1:
override.get("parameters") or override.get("inputSchema")treats a deliberate"parameters": {}override (stripping every param off a tool) as falsy and silently drops it — the exact bug class this issue was filed for, reproduced in miniature by the fix meant to close it. Switched to explicit key-presence checks.parametersvalue doesn't fail at assignment time (fastmcp doesn't validate on plain attribute assignment) — it fails later, in unrelated code that lists a server's tools (to_mcp_tool(), called fromfastmcp's own tool-listing path), breaking tool discovery for that namespace's server until the override is reset. Added anisinstanceguard before the assignment ever happens, plus the same check in the API validator for immediate write-time feedback (422) instead of a silent no-op followed by a later, unrelated crash.Round 2 found the same failure shape had two more instances:
description— identical crash-later behavior via the exact same mechanism, just an unguarded sibling field. Verified directly:tool.description = 12345succeeds silently,tool.to_mcp_tool()then raisespydantic.ValidationError.{"send_email": "not even a dict"}) — this one is worse:override.get("description")sits outside the function's owntry/except, so a non-dictoverrideraisesAttributeErroruncaught, crashing_apply_tool_overridesentirely — which meanscreate_mcp_serverfails, not just tool listing. Fixed by rejecting this at the API boundary (422, so it never reaches the DB from this write path) and defensively skipping (not crashing) at apply time, since nothing guarantees every row in the DB went through the validated path (a future direct write, a seed script, a migration).All three failure shapes — non-object override entry, non-string description, non-dict parameters/inputSchema — are now rejected both at write time (clear 422) and, defensively, at apply time (skip with a warning log, never crash).
Test plan
tests/unit/mcp/test_factory.py(new, 15 tests): description-only override still works; parameters-only override actually lands ontool.parameters; the override reaches the wire-protocol schema viatool.to_mcp_tool().inputSchema(end-to-end proof);inputSchemaaccepted as an alias; both together; parameters-only doesn't touch description; a deliberate{}override isn't dropped; a non-dictparametersis rejected without crashing (andto_mcp_tool()still succeeds afterward, proving nothing bad landed);parameterswins overinputSchemawhen both present; a non-stringdescriptionis rejected the same way; a non-object override entry is skipped without crashing_apply_tool_overrides, and doesn't block other tools in the same batch; unknown tool name, empty overrides, and no-providers cases all handled cleanlytests/unit/apps/test_darklab_tool_overrides.py(new, 9 tests):ToolOverridesUpdate's validator accepts all valid shapes (including deliberate empty-dictparameters) and rejects non-dictparameters/inputSchema, non-stringdescription, and non-object override entries, including when the bad entry is one of several tools in a batchpytest tests/unit/mcp/ tests/unit/ctf/ tests/unit/apps/ -q— 64 passed, 1 pre-existing unrelated skippytest tests/unit/ -q— full suite, 330 passed, 4 pre-existing failures unrelated to this change (present before it too)origin/main: clean on its own, and clean paired individually with each of Fix: orchestrator confirms payment to vendor even when it fails #574, Fix: DNS-based SSRF bypass in guardrail webhook URL validation #575, and Fix: guardrail payload corruption before signing + missing after_tool for complete_task #576 (no new conflicts introduced by this branch; the only conflict in the batch remains the one already disclosed between Fix: DNS-based SSRF bypass in guardrail webhook URL validation #575 and Fix: guardrail payload corruption before signing + missing after_tool for complete_task #576, unrelated to this PR)