Skip to content

Webhooks cannot target explicitly trusted private services in Production #30893

Description

@muhannad0

Issue Summary

Self-hosted Ghost instances running in production cannot deliver webhooks to explicitly trusted services on the same private Docker network.

Ghost rejects webhook targets whose hostnames resolve to private IP addresses:
URL_PRIVATE_INVALID: URL resolves to a non-permitted private IP block

Please consider supporting an configuration controlled allowlist for exact private webhook hostnames and ports.

Steps to Reproduce

  1. Create a private Docker network:
docker network create ghost-network
  1. Start a MySQL 8.4 container:
docker run -d \
    --name ghost-mysql \
    --network ghost-network \
    -e MYSQL_ROOT_PASSWORD=ghost-root-password \
    -e MYSQL_DATABASE=ghost \
    -e MYSQL_USER=ghost \
    -e MYSQL_PASSWORD=ghost-password \
    mysql:8.4
  1. Start a webhook receiver on the same network:
docker run -d \
    --name webhook-receiver \
    --network ghost-network \
    example/webhook-receiver:latest

The receiver listens on port 3000 and exposes: POST /api/webhooks/posts

  1. Start Ghost in production mode on the same network:
docker run -d \
    --name ghost \
    --network ghost-network \
    -p 2368:2368 \
    -e NODE_ENV=production \
    -e url=http://localhost:2368 \
    -e database__client=mysql \
    -e database__connection__host=ghost-mysql \
    -e database__connection__port=3306 \
    -e database__connection__user=ghost \
    -e database__connection__password=ghost-password \
    -e database__connection__database=ghost \
    ghost:6.63.0
  1. Verify that Docker DNS resolves the receiver from the Ghost container:
docker exec ghost node -e \
    'require("dns").lookup("webhook-receiver", console.log)'
The hostname resolves to a private Docker network address such as:

null 172.18.0.3 4
  1. Verify that the receiver is reachable from the Ghost container:
docker exec ghost node -e '
const http = require("http");

const request = http.request(
    "http://webhook-receiver:3000/api/webhooks/posts",
    {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        }
    },
    response => {
        console.log("Status:", response.statusCode);
        response.resume();
    }
);

request.on("error", console.error);
request.end("{}");
'
  1. In Ghost Admin, create a custom integration and configure a webhook with this target URL:
    http://webhook-receiver:3000/api/webhooks/posts

  2. Trigger the webhook by performing the configured event, such as editing or publishing a post.

  3. Observe that Ghost rejects the destination because it resolves to a private Docker network address.

The validation occurs in errorIfHostnameResolvesToPrivateIp():
https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/lib/request-external.js

Ghost Version

6.63.0

Node.js Version

22.23.2

How did you install Ghost?

Official ghost:6.63.0 Docker image running on a self-hosted Docker host.

Database type

MySQL 8

Browser & OS version

Not applicable.

Relevant log / error output

Triggering webhook for "post.edited" with url "http://webhook-receiver:3000/api/webhooks/posts"

ERROR [WEBHOOK_DELIVERY_FAILURE]
url=http://webhook-receiver:3000/api/webhooks/posts
status=none
error_code=URL_PRIVATE_INVALID
message=URL resolves to a non-permitted private IP block

RequestError: URL resolves to a non-permitted private IP block
    at Request._beforeError
    at Request.flush
    at errorIfHostnameResolvesToPrivateIp
    at async Request._makeRequest

Code of Conduct

  • I agree to be friendly and polite to people in this repository

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

    needs:triage[triage] this needs to be triaged by the Ghost team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions