XML External Entity (XXE) injection in ProcessDiagramLayoutFactory

Author: geo-chenCreated Jun 15, 2026Updated Jun 15, 2026

Summary

ProcessDiagramLayoutFactory.parseXml(InputStream) builds a DocumentBuilderFactory with no XXE protections and parses the BPMN XML of a process definition. Unlike BpmnXMLConverter, this parser is not covered by any "safe XML" flag, so a DOCTYPE with an external entity in a deployed BPMN resource is resolved when the process diagram layout is computed. A user able to deploy a process definition can read arbitrary local files and perform server-side requests (SSRF) from the engine host, even when the rest of the engine is configured for safe XML handling.

Details

The diagram-layout parser in modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/diagram/ProcessDiagramLayoutFactory.java creates a default DocumentBuilderFactory and immediately parses the input stream:

java
protected Document parseXml(InputStream bpmnXmlStream) {
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(true);
    DocumentBuilder builder;
    Document bpmnModel;
    try {
        builder = factory.newDocumentBuilder();
        bpmnModel = builder.parse(bpmnXmlStream);   // <-- DTD + external entities enabled by default
    } catch (Exception e) {
        throw new FlowableException("Error while parsing BPMN model.", e);
    }
    return bpmnModel;
}

None of disallow-doctype-decl, external-general-entities, external-parameter-entities, load-external-dtd, setExpandEntityReferences(false), or XMLConstants.ACCESS_EXTERNAL_DTD are set. The JAXP defaults resolve external entities, so a SYSTEM entity reference in the parsed XML is fetched and inlined into the DOM.

The entry point is RepositoryService.getProcessDiagramLayout(processDefinitionId):

java
// GetDeploymentProcessDiagramLayoutCmd.execute
InputStream processModelStream = new GetDeploymentProcessModelCmd(processDefinitionId).execute(commandContext);
InputStream processDiagramStream = new GetDeploymentProcessDiagramCmd(processDefinitionId).execute(commandContext);
return new ProcessDiagramLayoutFactory().getProcessDiagramLayout(processModelStream, processDiagramStream);

processModelStream is the raw BPMN resource that was stored at deployment time. Deployment stores the original bytes, so a <!DOCTYPE ... > block survives in the deployed resource even though the primary BpmnXMLConverter parse path hardens its own XMLInputFactory. When the layout is later computed, parseXml re-parses those stored bytes with the unhardened factory and the external entity fires.

The same unhardened pattern is present in the Flowable 5 compatibility engine at modules/flowable5-engine/src/main/java/org/activiti/engine/impl/bpmn/diagram/ProcessDiagramLayoutFactory.java.

PoC

(available upon request)

Impact

Any principal permitted to deploy a process definition (a modeler/deployer role, not only a full engine administrator) can embed an external entity that is resolved by the diagram-layout parser. This yields arbitrary local file disclosure (engine configuration, credentials, key material) and server-side request forgery against internal network endpoints from the engine host. The parser is not protected by the enableSafeBpmnXml flag, so safe-XML deployments are still affected. Scope is changed because the leaked data and outbound requests target resources outside the deploying principal's authority.

Source: flowable/flowable-engine