Repository navigation
Open
Description
Follow-up to #17. A security and implementation review of the MCP tools found the points below. The urgent ones are fixed in #17 (all MCP tools admin-only, mcp.deploy switch, destructiveHint on irreversible tools, api-platform/core ^4.3.18, nested {} in tool arguments). The rest is hardening and cleanup; none of it is exploitable with the default configuration (all switches off).
Security hardening
- Configuration lookup (
FlowableClientLocator::resolveExplicit()): ignoresactive, so deactivated engines stay usable with their credentials; the prefix goes unescaped intoLIKE(%,_); the error messages distinguish "not found", "type X" and "ambiguous" and name other configuration types, which makes a prefix oracle over allApiConfigurations. Accept only[0-9a-f-]{8,36}, filter ontype=flowableandactive=truebefore matching, return one generic error. Add thepatternto the{uuid}branch of the input schemas too. - Input schemas of read tools are advertised, not enforced: the handler runs with
validate=falseand the providers do not validate, soadditionalProperties: falseand theapiConfigurationpattern are documentation only for the read tools andflowable_dmn_deploy(uploadedResource()). Validate MCP arguments in the providers, or document the schema as advisory; the docblock ofMcpToolInputSchemaFactoryclaims the opposite. - IDs from tool arguments are not validated (
TimerJobExecuteProcessor,TaskCompleteProcessor,ExecutionTriggerProcessor,CompositeRowsTrait):.and..surviverawurlencode()and HttpClient removes dot segments, so a caller can pick the upstream endpoint; an arrayidbecomes"Array"with an E_WARNING. Add a shared helper:is_stringplus^[A-Za-z0-9][A-Za-z0-9:._-]*$. - File name of single-file MCP uploads (
AbstractFlowableProcessor::uploadedResource(),FlowableClient::multipartBody()): only the extension is checked;", CR/LF,/and..reachContent-Dispositionunescaped. Reuse the name rule ofDeploymentBundleProcessor::files(), extended by quotes and control characters, and escape inmultipartBody(). - Upstream errors go to the agent unfiltered (
FlowableApiException): engine exception bodies and transport errors with internal URLs end up in the JSON-RPC error. Return a generic message with a correlation id and log the details. - Switches and the metadata cache: the switches are baked into the cached metadata. With
%env(bool:...)%a switch turned off withoutcache:clearleaves the tools active. Reject env placeholders or document it. - Size limits for inline content:
mcpBundle.input.jsonhas nomaxItems/maxLength, and the payload is held in memory several times inDeploymentBundleProcessor. Add schema limits and a cap on the decoded bytes. -
.bar/.zipviaflowable_dmn_deploygo to the engine unchecked. Allow only.dmnover MCP. - Optional BPMN lint before deploy: reject or flag
scriptTask,flowable:type="shell|http",flowable:classand expressions in deployments that come in over MCP. - Audit: the audit context has no transport (
via=mcp) and no tool name, and only successes are logged, so agent and human actions cannot be told apart. Add both; log attempts, with a sha256 of deployed content. - Free-form deployment metadata:
tenantId,categoryanddeploymentSourceinDeploymentBundleProcessorare free strings;deploymentSourcecan be spoofed. Prefix server-side or check against an allowlist (relevant for multi-tenant engines). - Object-level access for reads: the HTTP read operations give every
ROLE_USERall tasks and up to 200 variables of every instance, with no check on assignee, starter or tenant. MCP is admin-only now; the HTTP side still needs a voter or a dedicated reader role, and a variable denylist. -
McpToolInputSchemaFactoryapplies app-wide without the namespace guard of the switch factory; a broken path throws and takes down the whole tool registry;..is not rejected. -
McpResourceentries are not switched; future resources have no fail-closed default. TreatMcpResourceas read. - Rate limiting: document a Symfony RateLimiter in front of
/mcpin the README. - Test: no test sends a real
tools/callasROLE_USERto an admin tool;McpToolMetadataTestcompares the security expression as a string only. Add a kernel or handler test with access denied.
Implementation
-
OperationInputTrait: read tools dropapiConfigurationin the{uuid}form silently and fall back to implicit resolution, which gives a misleading error with several engines. MoveconfigurationId()into the trait. -
DeploymentBundleProcessor: only the last extension is checked, sorates.dmn.xmlandcase.cmmn.xmlpass despite the documented.dmnrejection. Match full suffixes. -
FlowProcessStatusProvider/FlowProcessHistoryProvider: executions have no*Total(README says otherwise); history sorts ascending, so the 200-row cap drops the newest activities;decisionsTotalis computed from the first page. -
flowable_dmn_evaluate: the description promises rows as "a map of output name to value", butnormalizeResult()passes the engine'sresultVariablesthrough (lists of{name, type, value});idempotentHint: trueholds only functionally, every call adds a history row. Fix the description or the normalisation; add a state test for the result shape. -
executeTimerJob(): verify against a live engine; "run now" needs an active async executor. -
CompositeRowsTrait::row()serialises by reflection and bypasses#[Groups]; normalise with the serializer and the read group.
Tests
- Switch decorator together with
CachedResourceMetadataCollectionFactoryandOperationMetadataFactory. - A test through
ApiPlatform\Mcp\Server\Handlerinstead of the hand-built context inStateTestCase. - Multipart HTTP tests for DMN and event uploads, a test for
TaskInputSchemaController, aMockHttpClienttest forexecuteTimerJob(). -
ext-zipinrequire-dev.
DRY
- Inline file decoding exists twice (
DeploymentBundleProcessor::files()anduploadedResource()) with different checks. -
schemaPath()andMcpToolInputSchemaFactory::path()build the same path. - The
apiConfigurationschema block is copied about 20 times and has already drifted (stringvsoneOf); the pagination block 3 times. - Advertised schema (extra properties) and enforced schema (
schemaPath(...)) are separate references; a test should check they match.
Documentation
- "failures are tool errors" in several tool descriptions is wrong: the handler returns JSON-RPC errors, not
isError. - README:
oneOfis dropped from the input schemas as well, not onlyallOf/anyOf. -
structuredContentis JSON-LD while theoutputSchemais built forjson; setoutputFormatsor document it. - The priority comment in
config/services.yamlfor the switch decorator is ambiguous. - The MCP branches in
DeploymentUploadProcessorandEventDeploymentUploadProcessorare used by no tool yet; theprocessDefinitionfilter onFlowExternalWorkerJobis unrelated to MCP.
Metadata
Metadata
Assignees
Labels
No labels
Activity
schmunk42 commentedon Nov 1, 2026
Status after 0.11.0 (#21): every MCP tool now checks its own permission (
flowable:mcp_read,flowable:deploy, …) instead of a role, so an application without a matching voter denies all tools. The test suite sends realtools/callrequests and verifies access denied without the permission, which covers the last item under "Security hardening" (a realtools/callas an unprivileged user). 0.11.0 also enforces the permission in the three custom controllers that skipped theirsecurityexpression before.The remaining hardening, implementation, test, DRY and documentation items above are still open. Keeping this issue open for those.