[Plugin Request] Stripe webhook verification

Stripe recommends the verification of all webhook messages. This is important to ensure that Stripe sent the message, not someone else.

Stripe generates signatures for each message. According to Stripe documentation, the Stripe-Signature header included in each signed event contains a timestamp and one or more signatures that you must verify. The timestamp is prefixed by t= , and each signature is prefixed by a scheme . Schemes start with v , followed by an integer. Currently, the only valid live signature scheme is v1 . To aid with testing, Stripe sends an additional signature with a fake v0 scheme, for test mode events.

Stripe-Signature:
t=1492774577,
v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd,
v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39

Stripe generates signatures using a hash-based message authentication code (HMAC) with SHA-256. To prevent downgrade attacks, ignore all schemes that aren’t v1.

Currently, no plugin verifies the message, and the Stripe notifications blog post in Bubble documentation recommends to “run without authentication for simplicity”.

Could you please include a message verification action in Stripe’s plugin?

The documentation is available at the following link:

https://docs.stripe.com/webhooks?lang=node#verify-webhook-signatures-with-official-libraries

1 Like

Check out this topic for methods to authenticate Stripe webhooks

You don’t need a plugin to securely authenticate webhooks.

2 Likes

Thanks for this information; it’s very useful.
However, sending the API token in the URL is unsecure, since it’s sent through the internet in plain text. Checking the event ID + IP really helps.

Then don’t…. use the Stripe check event API call to get the event ID from Stripe and use that action’s data in your backend workflow rather than the webhook’s request data.

1 Like

The Bubble docs you referenced also state the following (which @georgecollier also mentions)…

However, I do think your concern is legitimate. After all, what’s to prevent a malicious actor from bombarding a Bubble webhook endpoint with payloads having valid but stale event ids, thereby causing your Bubble logic to do things it shouldn’t.

Mind you, most Stripe object ids - including event ids - are considered by Stripe to be “safe to share”; so it’s not inconceivable that event ids could be discovered “in the wild” and then somehow associated with the relevant public endpoint.

I think the bottom line [and unfortunate reality] is that it’s simply not currently possible to “properly” verify Stripe webhook events using just Bubble; so it boils down to the nature and level of risk one’s willing to accept.

None of the “hoop jumping” will guarantee - like signature verification would - that the payload arriving at your Bubble endpoint is both legit and unaltered.

Having said all that, one option I haven’t seen mentioned is employing a “relay” mechanism. That is, set up the Stripe webhook using a cloud function or a 3rd-party platform (Hookdeck? Make?) where proper signature verification can happen, and then repost the payload to your authenticated Bubble endpoint (putting the auth token in the header of course - not the URL).

Anyway, just wanted to share a few thoughts. I’ll end with a quote directly from Stripe support:

:point_up:

Request comes in → make API call to Stripe with request’s event ID → use result of API call (rather than request data) will always mean that the data is coming directly from Stripe.

1 Like

But it doesn’t mean it’s the “right” data. Please re-read this…

Then you check the time of the request (which is included in the event data), and invalidate outdated requests, or, store processed event IDs in your Bubble DB and check that the request hasn’t already been dealt with.

As I said, you can jump through hoops, but my comments (and those of Stripe support) stand. (IMO, this is an example where Bubble makes something that should be simple harder.)

EDIT

Not only that, but you don’t think a truly savvy bad actor would think to change some timestamps? Heck, it’s the first thing I’d do, and I’m not malicious…usually.
:smirk:

EDIT 2

Ok, I see you’re referring to checking the timestamp of the request that’s in the response you get from Stripe. Yes, that’s better than nothing but still no substitute for proper verification.

So you’re telling me that you could do all of the following:

  1. check event from Stripe and use data directly from Stripe
  2. check time is recent
  3. check and verify IP address of request from request header against known Stripe IP addresses
  4. check the event hasn’t already been processed by the Bubble app by storing the processed event IDs in the DB

…and it’s still not pretty damn secure? Especially when event IDs are generally never seen by the client?! Find me a case where you could get through all four steps and I’ll happily be proven wrong though :wink: Of course it’d be nice to verify how Stripe wants it natively, but in the absence of that you can still get very good security through other means.

your webhook is supposed to handle duplicate events anyway because there is the possibility that stripe will send the request multiple times

Fair enough, but how many Bubble apps actually implement idempotency. I suspect few if any. Doesn’t really change my thoughts on proper webhook verification though.

Just wanted to note that this must be done properly for Bubble sites behind a reverse proxy that are using the proxy domain for the webhook endpoint. Otherwise, legitimate webhook events could be missed.

Specifically, in addition to checking the known Stripe IP addresses against the cf-connecting-ip header, they should also be checked against the first entry in the x-forwarded-for header as well.

No doubt an uncommon setup, but might be relevant for users of CoAlias or similar. Anyway, for what it’s worth… :neutral_face:

Necroing this thread with a possible solution for anyone who stumbles upon it.

In typical bubble fashion, they ALMOST made this possible. If you both:

  1. Use “detect request data”
  2. Check the “Include headers in the detected data” option

You will have access to a “raw body text” field from bubble. This field could, in theory, be used to verify the request came from Stripe. However, unfortunately, this field is not ACTUALLY the raw body text, but a formatted JSON object.

E.g. stripe sends an event in the following format:

{
  "id": "1234",
  "object": "event",
  "field3": [
  ]
}

But the “raw body text” will result in:

{"id":"1234","object":"event","field3":[]}

Stripe’s signature library requires that the body text be EXACTLY the same, so this isn’t super helpful. You can use JSON.stringify(JSON.parse(data),null,2) to recreate it ALMOST perfectly, but your empty objects and arrays (i.e. [ ] and {}) will still be formatted differently.

At this point I gave up, but if someone was willing to write code to convert the empty arrays and objects to the same format that Stripe does, it in theory should work. Of course, you’d be subject to Stripe’s whim on if they ever change their format.

I have let bubble know about this, but knowing their team, they probably won’t fix it.

EDIT: To clarify, once you have the request body in the correct format, you use it in conjunction with the signature header and webhook secret to validate the request, which you can do using Stripe’s node.js library in a server-side plugin action

EDIT2: I’m making an educated guess on the actual Stripe body format based on info reported by Stripe. If it turns out their actual body text is different from what they report in the dashboard, then the solution would be to parse to conform to that instead.

1 Like

this has been already discussed in the forum: you can “massage” the “fake” raw request to make it pass the verification with the stripe sdk.
but that’s the opposite of what that verification is supposed to do: check the validity of the data as is without changing it in any way.

True, and it would technically allow for some amount of spoofing, since any string that could be parsed to produce the same result would pass. However, it does seem to be the only option for getting the request verified via Stripe’s signature.

I while ago I built a plugin that does exactly this (takes the minified raw body request and reconstructs it back into the exact format it was sent in by Stripe), so it can be used to verify webhooks the approved way.

At the time, I had a lengthy discussion with Stripe support who (as well as recommending NOT to use Bubble at all with Stripe, due to it’s inability to secure Webhooks the ‘approved’ way) confirmed that the format of the API request body I was using was correct - but also said they couldn’t guarantee it would stay that way, nor would any updates to the request body format be included in their Developer Digest list of updates).

The plugin worked great… for about 6 months, then stopped working, no doubt due to a change in their body formatting (I never bothered to check).

But that’s the trouble doing it this way… even if you can figure out how to reconstruct the request body correctly you’re at the mercy of Stripe keeping the formatting the same.

It still baffles me that such a simple, and important thing, as being able to correctly verify webhook signatures is impossible to do properly in Bubble.

true, but because it’s not a correct way to verify the webhook you have to accept the fact that you can’t trust webhook data if you use bubble.
the solution for me is to treat the webhook as a trigger and make a api request to stripe to get the data securely. to reduce people randomly hitting your endpoint you can block the workflow if the ip address of the request is not one listed by stripe.

1 Like