How to ensure security in Stripe webhooks?
Asked Answered
E

4

6

I would be creating Stripe web hooks and want to know how to ensure security such that only Stripe can access my URL?

I am using Laravel with the Stripe library.

Ence answered 3/2, 2015 at 9:57 Comment(1)
Don't you have token stuff...?Juback
W
16

As noted in the documentation:

If security is a concern, or if it's important to confirm that Stripe sent the webhook, you should only use the ID sent in your webhook and should request the remaining details from the Stripe API directly. We also advise you to guard against replay-attacks by recording which events you receive, and never processing events twice.

In other words, there's no way to confirm where a request came from. As such, don't trust any of its data, except for the id of any given item. You then contact the Stripe API yourself, asking for the item with the given id. This way you know where you're getting your data from, and the webhook call essentially just becomes a notification letting you know you should check in with the API.

Ways answered 3/2, 2015 at 10:7 Comment(7)
It doesn't fully prevent foul play, but restricting access only to IP addresses which stripe lists, such as in Carlos Devia's answer provides an additional powerful layer of protection. An attacker would need to either spoof from those IP's or compromise one of them, both of which would be a huge feat. I think best practice (Stripe's documentation echoes this) would be to do both that AND what you suggest here.Gratify
The problem with white listing IPs is that Stripe must give a huge guarantee about the stability of that list, or there needs to be a way to keep up to date with it. But yes, if that’s given, sure.Ways
The list is published here: stripe.com/files/ips/ips_webhooks.txt and more documentation and more lists can be found here: stripe.com/docs/ipsGratify
I agree that only trusting the ID, then retrieving the rest of the data by API call is best practice. I was surprised that much of the official Stripe docs instruct users to the contrary. I also wonder why they even provide all the information with the event in the first place. Not only does it encourage users to adopt the less-secure approach of using the given data directly, but it also, in the case of best practices, incurs unnecessary overhead, sending a lot of data that will be discarded and then re-retrieved later.Gratify
TLS ensures that what the sender sent was not modified, so we have an integrity guarantee at the transport layer. The HMAC signature also ensures that only the pre-shared secret can be used or your verification of that signature will fail, so you have an additional integrity check at the application layer. Also you have an assumption that Stripe was compromised when you distrust the secret to make this even less likely you assume the TLS Certificate was also compromised. If these layered assurances are not enough for your trust, then you've already decided to not trust Stripe any longer periodPilar
@Pilar TLS is somewhat irrelevant here, only the secrecy of the signer secret provides some guarantee. TLS can’t prove who sent the webhook, so I don’t much care that the message wasn’t modified.Ways
@Ways HMAC is literally message authentication code, "authentication" implies who is authentic. When you talk about "message wasn’t modified" you are now talking about integrity, an entirely distinct concept from authentication. I think you are confusing your apples with stonesPilar
T
5

Actually you can limit the access to your webhooks only to the Stripe IPs. That's why Stripe have a list of webhooks ips updated.

https://stripe.com/docs/ips

I just did it for a project using Laravel and Stripe. You can do something like:

//Only accept connections from stripe ips.


    //Save stripe info for 24 hours for performance.
    $stripe_webhooks_ips = Cache::remember('stripe_webhooks_ips', 1440, function()
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_URL, 'https://stripe.com/files/ips/ips_webhooks.json');
        $result = curl_exec($ch);
        curl_close($ch);

        $json_result = json_decode($result,true);

        return $json_result['WEBHOOKS'];
    });



    if(in_array($_SERVER['REMOTE_ADDR'],$stripe_webhooks_ips)) {
        //Your Code Here.
    }

About the security of the remote_addr variable: Is it safe to trust $_SERVER['REMOTE_ADDR']?

Remember to take extra measures to get the origin ip if you are behind a proxy or a CDN service like Cloudfare, etc.

Toluate answered 3/2, 2016 at 19:25 Comment(0)
B
2

Stripe signs the events it sends to you, so you can validate the signature to check Stripe was the sender.

The Stripe libraries will handle this for you, including replay attack prevention. See the docs at https://stripe.com/docs/webhooks/signatures. There is a PHP example.

Idempotency is a separate matter. You could receive events more than once, which is your problem to deal with (by tracking event IDs). This is mentioned on this page: https://stripe.com/docs/webhooks/best-practices.

Blistery answered 12/9, 2019 at 16:12 Comment(0)
S
0

It has been previously mentioned but I you check check the signature of the requests with the webhook secret that stipe provides to you. The following example comes from Appwrite's function templates. You might check the source code of the validation function on GitHub.

validateWebhook(context, req) {
    try {
      const event = this.client.webhooks.constructEvent(
        req.bodyRaw,
        req.headers['stripe-signature'],
        process.env.STRIPE_WEBHOOK_SECRET
      );
      return /** @type {import("stripe").Stripe.DiscriminatedEvent} */ (event);
    } catch (err) {
      context.error(err);
      return null;
    }
  }
 const event = stripe.validateWebhook(context, req);
if (!event) {
  return res.json({ success: false }, 401);
}
Shiism answered 25/4 at 11:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.