Stripe webhook security

I have been implementing some Stripe functionality to one of my apps and glanced at a couple of videos to make sure I’m on the right track… when I noticed that 3 videos in a row left the API workflow exposed to the public. No authentication or any security measures in the workflow to check whether it is actually Stripe communicating to that endpoint.

So at this point I started questioning myself. Am I being paranoid or all these content creators are missing a crucial step in implementing Stripe payments?

Stripe themselves recommend to verifying the events sent through webhooks.

If you are listening for Payment Intent events with a “success” status on your webhook to verify payments and you have no auth/security in place, all an attacker has to do is obtain a Payment Intent ID (which is actually exposed by Stripe in the redirect URL after a successful or failed 3DS) and post a Payment Intent Event with the ID to the webhook to complete the process without payment.

Let me know if I’m missing something here… but I did implement some verifications.

2 Likes

You will need some verification process - there’s a forum post here that discusses different approaches - Stripe Webhook in Bubble - big vulnerability to all apps that use them

1 Like

the reality is that bubble does not go well with the official verification that stripe says you should do, not even with code.
Therefore you can’t trust the data received in the webhook.
A possible option is to make a request to stripe and use that as data. For example: receive a webhook that confirms a completed checkout, take the checkout id from the webhook, make a request to stripe to retrieve the checkout, use the response as the data.
Add a condition on the workflow checking that the webhook request is coming from known stripe ips, so random bots hitting the webhook don’t make you call stripe for nothing.
It’s not ideal because the whole point of webhooks is to reduce api calls, but it’s better than trusting the data without checks.

1 Like

Interesting thread, thanks

This is the solution I went with. I make requests to stripe whenever the webhook receives data to double check it’s validity. Totally defeats the purpose of the webhook, but we have to work with what we’ve got. Checking the IPs is a great addition, thanks.
I did implement Stripe in a python project a while back and the whole signature validation process is quite simple. Bubble should make an effort and provide us with the ability to do that and also spread the word so that we don’t end up with tens of Youtube tutorials with gaping security holes.

3 Likes

I’ve made numerous requests to Bubble over the past 9 months asking for them to make the very simple change needed to enable the correct validation of Stripe Webhook Signatures (as per the Stripe docs) in Bubble… but so far it’s fallen on deaf ears.

(which is crazy given a. how simple it should be to validate Stripe Webhooks, b, how common the use of Stripe is amoungst Bubble users, and c. how big of a security issue this potentially is, especially for some apps).

So I’m not holding my breath on this one…

11 Likes

I wish there was a way we could get this escalated. I just started implementing webhooks in order to manage my subscription system and immediately saw the benefits. However, despite not being a security expert, even I immediately recognized the potential vulnerability.

Ironically, I only went down this path because of some drawbacks regarding the native Stripe plugin that I was surprised weren’t built in.

1 Like

A great plugin by @jonah.deleseleuc that gives you the tools and a video walkthrough tutorial on how to implement it.

4 Likes

Yeah my plugin Security Salamander does exactly what you’re looking for :slight_smile: you will have to understand how to read Stripe’s documentation (and how they recommend securing your endpoint) in order for it to work. This requires some learning, but you can do it :smiley:

2 Likes

You can secure both Stripe & Plaid webhooks using security salamander :wink:

I’m curious about how do you handle the fact that bubble transforms the raw body of the request before it’s available to any workflow/plugin.
From Stripe’s docs:

Stripe requires the raw body of the request to perform signature verification. If you’re using a framework, make sure it doesn’t manipulate the raw body. Any manipulation to the raw body of the request causes the verification to fail.

From a quick test it seems that the “raw body” exposed by bubble fails the verification both when using the stripe library or the manual verification.

2 Likes

Yeah, currently Bubble manipulates the raw body (by removing all spaces, line breaks, and indents) such that it no longer matches the body sent by Stripe (and therefore fails the verification).

It is possible to try to re-format the raw body back into the way it was originally sent, but it’s pretty much guesswork and not very consistent (and besides it’s just stupid and unnecessary trying to do that).

If Bublble just gave us access to the raw body (without manipulating it first) then securing Stripe Webhooks as explained in the Stripe docs would be very simple to do in Bubble (as it should be).

I have looked at your plugin before (and watched your video several times), but I’ve not yet understood how you can use this with Stripe Webhooks (at least not in the way described by the Stripe Docs), as there’s no way (unless I’m missing something obvious) to access the raw body from the webhook in the way it was actually sent by Stripe…

Do you have any working example, or video of how you’re able to do this with Stripe?

1 Like

When it’s necessary to check the integrity of the raw body, those plugin actions will automatically format it in such a way that it can pass the API providers checks (granted if the api call is a legitimate one). While technically this somewhat less secure because the person making the fraudulent api call doesn’t need to worry about using tab’s vs double space, the rest of the security protocols are still in place. I am using Security Salamander in a live app right now, and although it costs a lot of WU’s to check every single API call, the added security benefit from it is 1000% worth it.

2 Likes

Sure thing :smiley: I will make a video and get back to you!

1 Like

Unfortunately I believe that Bubble will never implement a native solution to securing your webhooks. First of all, I would say we are a minority of users who use webhooks. Second of all, we can technically secure our webhooks using custom code. Lastly, I can’t see how they would implement such a feature since every API uses a different way to secure endpoints and they are subject to change.

What would help is if Bubble made it less difficult to do what Security Salamander does. For instance, raw body should be, well, raw (i.e unchanged). Another thing that would help is to fix the Return Data action. This action does not occur in the order that it should - it always executes at the end of the workflow no matter where you place it. If they made more improvements like that, then we would be more than fine :smiley:

1 Like

Thanks for the reply.
I assume you are reformatting the body using the JSON.stringify options and taking care of a couple of differences with the formatting of empty arrays and objects (at least when testing with simulated events from the cli). When you hit the right formatting both the manual check using nodejs crypto and the automatic check with stripe-node work. I personally don’t find it very resilient: if Stripe changes the formatting of the body tomorrow the webhooks will fail and the app will miss important events, and Stripe does not need to warn about that because we are supposed to compare the raw body, not a reconstructed one.

1 Like

A lot of these services use JWT’s and are unlikely to make changes to their webhook validation protocols without warning the users first. I get your point about the fact that we aren’t supposed to use a reconstructed one though and that could break it. But if you’re using Bubble, you’re already relying on a ton of things not to change so I don’t see why this can’t be at least a medium term solution

1 Like

Here is a working example (validation of Plaid webhooks) where I let the user define the JSON object:

Here I hash it:

Screenshot from 2023-07-03 13-12-32

Here in my condition I check if it’s valid:

I am currently writing a tutorial article on how to do this. But in the meantime, you can follow these steps defined by Stripe and security salamander to validate your webhook

Verifying signatures manually

The Stripe-Signature header included in each signed event contains a timestamp and one or more signatures. 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

Note

Note that newlines have been added for clarity, but a real Stripe-Signature header is on a single line.

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

It is possible to have multiple signatures with the same scheme-secret pair. This can happen when you roll an endpoint’s secret from the Dashboard, and choose to keep the previous secret active for up to 24 hours. During this time, your endpoint has multiple active secrets and Stripe generates one signature for each secret.

Although it’s recommended to use our official libraries to verify webhook event signatures, you can create a custom solution by following these steps.

Step 1: Extract the timestamp and signatures from the header

Split the header, using the , character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a prefix and value pair.

The value for the prefix t corresponds to the timestamp, and v1 corresponds to the signature (or signatures). You can discard all other elements.

Step 2: Prepare the signed_payload string

The signed_payload string is created by concatenating:

  • The timestamp (as a string)
  • The character .
  • The actual JSON payload (that is, the request body)

Step 3: Determine the expected signature

Compute an HMAC with the SHA256 hash function. Use the endpoint’s signing secret as the key, and use the signed_payload string as the message.

Step 4: Compare the signatures

Compare the signature (or signatures) in the header to the expected signature. For an equality match, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.

To protect against timing attacks, use a constant-time string comparison to compare the expected signature to each of the received signatures.

I’m sure we all know how to validate a Stripe Webhook as per their docs… it’s very simple to do outside of Bubble…

But Bubble does not expose the raw request body…

So the issue is how do you access the raw request body in order to determine the expected signature?

2 Likes