Description
I dug into how async notifications are delivered when a user picks WebHook on /user.
What happens: NotificationService posts the notification straight to user.webHook via the default Nest HttpModule / axios client (notification.service.ts). When delivery is switched to WebHook, the URL is only validated as a non-empty string in PatchUserDto; there is no scheme/host policy, no SSRF blocklist, and no dedicated timeout or redirect limits on that HTTP client.
So any authenticated caller who can set their profile can make the Identity Service originate HTTP requests to arbitrary targets (loopback, RFC1918, cloud metadata endpoints, internal DNS names, etc.), with typical axios redirect-follow behavior on top. Worst case this is classic SSRF against the deployment’s network; even in benign setups, a slow or malicious endpoint can tie up the worker until the socket gives up.
What I expected: Outbound webhook calls treated as untrusted egress: bounded timeouts; HTTPS by default (or HTTP only behind an explicit dev flag); redirects disabled or capped; and a deny/allow policy for obviously internal/reserved destinations (with env toggles for edge cases). Docs should spell out the behavior.
I searched this repo’s issues and PRs for SSRF / outbound / notification HTTP and didn’t find this angle — sorry if I missed a duplicate.
Steps to reproduce
- Run Heka Identity Service with Postgres and normal auth (same as
docs/setup.md).
- Obtain a JWT for a user who can
PATCH /user (or your equivalent setup).
- Set
messageDeliveryType to WebHook and webHook to a URL you control on the same host or on a non-routable/internal address reachable from the process, e.g. http://127.0.0.1:9999/test (or another container on the same Docker network).
- Trigger any flow that emits a notification (connection / credential / proof state change — whatever your integration uses).
- Observe outbound traffic from the Identity Service toward that URL (listener logs, tcpdump, container logs). You should not see a rejection from internal URL validation, because none is implemented.
Optional: set webHook to an endpoint that returns 302 to an internal URL and confirm behavior matches axios defaults unless the client has been customized.
Additional context
Relevant code
heka-identity-service/src/common/notification/notification.service.ts — this.httpService.axiosRef.post(webHook, notification)
heka-identity-service/src/common/notification/notification.module.ts — plain HttpModule import
heka-identity-service/src/user/dto/patch-user.dto.ts — webHook validated with @IsString() + @IsNotEmpty() under ValidateIf, not URL / egress policy
Why it matters: Reference deployments often run in shared VPCs or Kubernetes. “Server invokes user-supplied callback URL” is a routine audit theme. Fixing this aligns the stack with production expectations without changing VC protocols themselves.
No single log excerpt attached reproduction is easiest with a tiny HTTP sink and process/network logs:
# Example sink (run somewhere the Identity Service can reach it)
python -m http.server 9999
Description
I dug into how async notifications are delivered when a user picks WebHook on
/user.What happens:
NotificationServiceposts the notification straight touser.webHookvia the default NestHttpModule/ axios client (notification.service.ts). When delivery is switched to WebHook, the URL is only validated as a non-empty string inPatchUserDto; there is no scheme/host policy, no SSRF blocklist, and no dedicated timeout or redirect limits on that HTTP client.So any authenticated caller who can set their profile can make the Identity Service originate HTTP requests to arbitrary targets (loopback, RFC1918, cloud metadata endpoints, internal DNS names, etc.), with typical axios redirect-follow behavior on top. Worst case this is classic SSRF against the deployment’s network; even in benign setups, a slow or malicious endpoint can tie up the worker until the socket gives up.
What I expected: Outbound webhook calls treated as untrusted egress: bounded timeouts; HTTPS by default (or HTTP only behind an explicit dev flag); redirects disabled or capped; and a deny/allow policy for obviously internal/reserved destinations (with env toggles for edge cases). Docs should spell out the behavior.
I searched this repo’s issues and PRs for SSRF / outbound / notification HTTP and didn’t find this angle — sorry if I missed a duplicate.
Steps to reproduce
docs/setup.md).PATCH /user(or your equivalent setup).messageDeliveryTypetoWebHookandwebHookto a URL you control on the same host or on a non-routable/internal address reachable from the process, e.g.http://127.0.0.1:9999/test(or another container on the same Docker network).Optional: set
webHookto an endpoint that returns 302 to an internal URL and confirm behavior matches axios defaults unless the client has been customized.Additional context
Relevant code
heka-identity-service/src/common/notification/notification.service.ts—this.httpService.axiosRef.post(webHook, notification)heka-identity-service/src/common/notification/notification.module.ts— plainHttpModuleimportheka-identity-service/src/user/dto/patch-user.dto.ts—webHookvalidated with@IsString()+@IsNotEmpty()underValidateIf, not URL / egress policyWhy it matters: Reference deployments often run in shared VPCs or Kubernetes. “Server invokes user-supplied callback URL” is a routine audit theme. Fixing this aligns the stack with production expectations without changing VC protocols themselves.
No single log excerpt attached reproduction is easiest with a tiny HTTP sink and process/network logs:
# Example sink (run somewhere the Identity Service can reach it) python -m http.server 9999