Skip to content
GitHub Security Advisory GHSA-fg6q-pfj7-fghw

GitHub Security Advisory GHSA-fg6q-pfj7-fghw

github.com • September 25, 2026

CordysCRM v1.7.3 contains a Server-Side Request Forgery (SSRF) vulnerability in the approval workflow webhook execution path. The ApprovalResourceService.sendWebHook() method makes outbound HTTP requests to the configured webHookUrl without performing any SSRF safety validation. While SSRF validation exists in the testConnect() method used by the webhook test connection endpoint, it is entirely absent from the actual runtime webhook invocation triggered when an approval action is performed. An attacker with approval flow configuration privileges can set the webhook URL to an internal network address, enabling internal network reconnaissance, cloud metadata exfiltration, and attacks against internal services.

The vulnerability exists in ApprovalResourceService.java at backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalResourceService.java. The trust boundary failure occurs between configuration storage and execution path: the webhook URL is stored in the database as part of the approval node post-configuration without SSRF validation, and when executed, the URL is consumed directly by HttpClientUtils without any safety check. Root Cause Chain: 1. Configuration entry (ApprovalFlowController.java:45-49): Admin creates/updates approval flow via POST /approval-flow/add or /approval-flow/update. The WebHookConfig (including webHookUrl) is embedded in the node postConfig JSON and saved to the database without SSRF validation. 2. Trigger (ApprovalFlowService.java:2224-2233): When an approval action occurs, updateApprovalPostField() retrieves the postConfig and calls resourceService.sendWebHook(). 3. Vulnerable execution (ApprovalResourceService.java:702-724): sendWebHook() parses postConfig JSON, extracts WebHookConfig, and if webHookEnable is true, directly passes webHookUrl to sendGet()/sendPost(). NO SSRF validation is performed. 4. HTTP sink (ApprovalResourceService.java:749): HttpClientUtils.sendGetRequest() uses Spring RestTemplate.exchange() to make the actual HTTP request. Contrast: testConnect() at line 982-986 DOES perform SSRF validation via ssrfValidationService.validate(), but it is only called from the optional test connection endpoint, not from the runtime webhook execution path. The SSRFValidationService provides comprehensive protection including protocol whitelist, private IP blocking, and loopback detection but is bypassed because it is never invoked during actual webhook execution. Core vulnerable code path:

java // backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalResourceService.java:702-724 @async public void sendWebHook(String postConfig, List fieldValues, ApprovalInstance instance) { ModuleFormConfigDTO formConfig = moduleFormCacheService.getBusinessFormConfig(instance.getType(), OrganizationContext.getOrganizationId()); ObjectMapper mapper = new ObjectMapper(); WebHookConfig webHookConfig; try { webHookConfig = mapper.readTree(postConfig) .get("webHookConfig") .traverse(mapper) .readValueAs(WebHookConfig.class); } catch (Exception e) { throw new GenericException(e.getMessage()); } if (webHookConfig != null && webHookConfig.getWebHookEnable()) { switch (webHookConfig.getWebHookMethod()) { case "POST" -> sendPost(webHookConfig, formConfig.getFields(), fieldValues, false, instance.getResourceId()); case "GET" -> sendGet(webHookConfig, formConfig.getFields(), fieldValues, false, instance.getResourceId()); default -> {} } } } This is the vulnerable method that executes webhooks without SSRF validation. The webHookUrl from the stored postConfig is passed directly to sendGet()/sendPost() without calling ssrfValidationService.validate().

java // backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalResourceService.java:982-1004 public void testConnect(WebHookConfig webHookConfig) { if (webHookConfig != null && webHookConfig.getWebHookEnable()) { HashMap resultObj = new HashMap<>(); // SSRF安全校验 ssrfValidationService.validate(webHookConfig.getWebHookUrl()); switch (webHookConfig.getWebHookMethod()) { case "POST": resultObj = sendPost(webHookConfig, null, null, true, null); if (MapUtils.isEmpty(resultObj)) { throw new GenericException("测试连接不通过"); } break; case "GET": resultObj = sendGet(webHookConfig, null, null, true, null); if (MapUtils.isEmpty(resultObj)) { throw new GenericException("测试连接不通过"); } break; default: break; } } } Contrast: testConnect() correctly calls ssrfValidationService.validate() before making the HTTP request. However, this method is only called from the optional test connection endpoint (POST /approval-flow/webhook/test), not from the runtime sendWebHook() path.

java // backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalFlowService.java:2224-2233 public void updateApprovalPostField(ApprovalInstance instance, String currentNodeId, ApprovalAction action, String currentUserId) { ApprovalResourceService resourceService = CommonBeanFactory.getBean(ApprovalResourceService.class); if (resourceService != null) { ApprovalNodeApprover approvalNodeApprover = approvalNodeApproverMapper.selectByPrimaryKey(currentNodeId); List resourceFvs = formService.compressResourceRefDetail(instance.getType(), instance.getResourceId()); resourceService.sendWebHook(action == ApprovalAction.APPROVE ? approvalNodeApprover.getPassPostConfig() : approvalNodeApprover.getRejectPostConfig(), resourceFvs, instance); resourceService.updateApprovalPostField(FormKey.ofKey(instance.getType()), instance.getResourceId(), action == ApprovalAction.APPROVE ? approvalNodeApprover.getPassPostConfig() : approvalNodeApprover.getRejectPostConfig(), currentUserId); } } This is the trigger point that calls sendWebHook() without any SSRF validation. When an approval action (approve/reject) is performed, the stored postConfig containing the webhook URL is retrieved and passed directly to sendWebHook().

Preconditions: An authenticated user account with PROCESS_SETTING_ADD permission; a business entity exists in the system that can trigger an approval flow. Steps: Step 1 - Create an approval flow with a malicious webhook URL via POST /approval-flow/add with body containing nodeConfigs[0].passPostConfig.webHookConfig.webHookUrl set to an internal target (e.g., ). Expected: HTTP 200, approval flow created. Step 2 - Trigger the approval flow by submitting a business entity for approval, then approve via POST /approval-action/approve with instanceId and nodeId. Expected: The server makes an HTTP GET request to the configured internal URL (SSRF triggered). Alternative internal targets include , , .

An attacker can exploit this to: 1. Exfiltrate cloud metadata (AWS 169.254.169.254, Azure, GCP) to steal IAM credentials; 2. Perform internal network reconnaissance and port scanning; 3. Attack unauthenticated internal services (Redis, MongoDB, Elasticsearch, Docker API); 4. Use the server as a pivot for lateral movement.

Extracted Entities