Skip to content

security(identity-service): user-configured notification webhooks lack SSRF / egress hardening #100

Description

@h30s

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

  1. Run Heka Identity Service with Postgres and normal auth (same as docs/setup.md).
  2. Obtain a JWT for a user who can PATCH /user (or your equivalent setup).
  3. 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).
  4. Trigger any flow that emits a notification (connection / credential / proof state change — whatever your integration uses).
  5. 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.tsthis.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.tswebHook 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions