跳到正文
DEV Community· HaoPeng Zhang·· 3 小時前AI 評分59

n8n Webhook URL 等同密碼,你可能曾把它貼到公開地方

Your n8n Webhook URL Is a Password — And You've Probably Pasted It Somewhere Public

AI 導讀

HaoPeng Zhang 分享 n8n Webhook 的安全做法,指出任何持有 Webhook URL 的人都能觸發工作流程,因此應把 URL 當作密碼保護。

正文

Your n8n Webhook URL Is a Password — And You've Probably Pasted It Somewhere Public

This is part of my build-in-public series on running LLM workflows in n8n in production. Everything here is from real templates I sell and support — the mistakes are mine, the fixes are tested.

The execution that shouldn't exist

A few months ago I was reviewing a client's execution list and found a run I couldn't explain. The workflow was a Webhook-triggered order processor — it only fires when their store sends an order event. But there it was: an execution at 3 AM, with a payload that didn't match their store's format at all. Garbage in, half-processed, then it failed on a missing field and paged me.

Someone — or something — had hit the webhook URL directly.

How? I never published the URL. But then I remembered: three weeks earlier I'd posted a screenshot of that workflow in a forum thread to illustrate a different problem. The Webhook node was visible. The full production URL was right there in the screenshot, readable at full resolution.

An n8n webhook URL is a bearer token. Whoever holds it can trigger your workflow. There is no second factor, no IP allowlist, no rate limit by default. My "private" workflow had been public since the day I shared that screenshot.

The three places webhook URLs leak

Once I started looking, I found my URLs in more places than I wanted to admit:

1. Screenshots and screen recordings. The #1 leak. You share a workflow to ask for help, the Webhook node is in frame, the URL is legible. Search any automation forum for webhook URL patterns and you'll find live ones.

2. Shared workflow JSON. Export a workflow and the Webhook node keeps its path, and anyone importing it into their own n8n gets... well, their own URL, actually — the domain changes. But the path stays, and if you've ever pasted the full URL into a Set node, a Code comment, or an HTTP node elsewhere, that travels with the JSON.

3. Logs and error messages. Webhook URLs end up in execution data, error payloads forwarded to alerting, and pasted into support tickets. Every copy is another bearer token in the wild.

n8n does give you separate test and production URLs, which helps — but most of us build against the production URL out of laziness and never switch.

The fix: treat the URL as a secret, then verify the caller anyway

Two layers. The first is hygiene; the second is the one that actually saves you.

Layer 1 — hygiene (do this today).

  • Turn on authentication on the Webhook node. n8n's Webhook node supports Header Auth — a shared secret sent in a header, checked before the workflow runs. It takes one minute to enable and it would have stopped my 3 AM execution cold.
  • Use the test URL while building, switch to production only when shipping.
  • If a URL leaks, change the webhook path. A new path is a new URL — the old one dies instantly. This is your rotation mechanism; use it like one.
  • Never post screenshots with the Webhook node expanded. Blur it or crop it. I learned this the expensive way so you don't have to.

Layer 2 — verify the signature (for webhooks you don't control).

Header Auth works when you choose the secret. But when the caller is Stripe, Shopify, or a client's store, they sign the payload instead — typically HMAC-SHA256 of the raw body with a signing secret, sent in a header. n8n won't verify that for you, so the first node after the Webhook must do it, before any side effect runs:

// Code node, first step after the Webhook trigger — fail closed
const crypto = require('crypto');
const secret = $env.WEBHOOK_SIGNING_SECRET; // never hardcode this
const rawBody = $json._rawBody ?? JSON.stringify($json.body);
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const received = $json.headers['x-signature-256'] ?? '';
const ok = received.length === expected.length &&
  crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!ok) throw new Error('webhook signature mismatch — rejecting');
return $json;

Three details matter here: verify against the raw body (re-serializing parsed JSON can change whitespace and break the HMAC), compare with timingSafeEqual (a plain === leaks the secret byte-by-byte to a patient attacker), and throw before anything else in the workflow touches the payload — no LLM call, no database write, no email.

Note the $env usage: the signing secret lives in an environment variable, not in the workflow JSON. Secrets in workflow JSON get exported, screenshotted, and committed to git. Ask me how I know.

The checklist I run now for every webhook workflow

  1. Webhook node has Header Auth enabled (or the caller uses HMAC signatures I verify in the first Code node)
  2. Signature check runs before any side effect, fails closed, uses timingSafeEqual on the raw body
  3. Signing secret in $env, never in the workflow JSON
  4. Building on the test URL; production URL only at ship time
  5. Webhook path treated as rotatable — changed immediately on any suspected leak
  6. No screenshots, exports, or logs containing a live production webhook URL

A webhook URL is a password that you paste into a browser address bar for a living. Treat it like one, and make the workflow distrust its caller until the signature checks out.


I'm building AI Automation Lab — production-ready n8n workflow templates. The free AI Hook Generator shows how I structure real workflows; my open-source templates live at github.com/zhp910318/n8n-ai-automation-lab.

來源:DEV Community · dev.to