2

Context

I am building a website in Python using the Django Framework and Stripe for user payment. I am currently In the testing/debug phase in local development, still far from a production build.

Currently at a brick wall with CSRF Protection and can not find a solution. My checkout works perfectly with no CSRF Protection.

There are other topics of very similar issues on here but due to my lack of knowledge around CSRF protection itself, I hope someone can enlighten me on what to do in my specific scenario.

This situation should have a common practice solution but I've studied the majority of both Django and Stripe's Documentation with no luck.

Criticism is welcome, this is my first StackOverflow post.


The Problem

I have a view function which enables the purchase of a product on my site by sending the user to Stripes External Checkout page.

views.py(outdated)

@csrf_exempt # CSRF Protection disabled for testing
def create_checkout_session(request):
    if request.method == 'POST':
        try:
            checkout_session = stripe.checkout.Session.create(

                ...

            )
        except Exception as e:

            ...

        return redirect(checkout_session.url)

During testing I disabled CSRF Protection on my view function using Django's @csrf_exempt decorator. I did this in order to bypass the '403 CSRF Verification failed' Error.

The Reason I was getting the error in the first place is due to this statement on Django's Documentation:

<form method="post">{% csrf_token %}

This should not be done for POST forms that target external URLs, since that would cause the CSRF token to be leaked, leading to a vulnerability.

Upon removing the @csrf_exempt decorator I am now stuck, because that is exactly how my submit form works, as seen in products.html below, the checkout url is generated by stripe (as seen in views.py) and is therefor (i think) an external url:

products.html

<form rel="noreferrer" action="{% url 'checkout' %}" method="POST">
    {% csrf_token %} <!-- forbidden in POST to external URL -->
    <button rel="noreferrer" id="checkout-button" class="btn btn-sm" type="submit">
        Buy
    </button>
</form>

Currently questioning the following things:

  • Is there an another method that can achieve the same result without using POST?
  • Have I misinterpreted the Stripe Documentation?
  • Is the checkout url actually 'external'?
  • Should I abandon this method and hardcode Stripe's checkout into my website for better security?

UPDATE

As requested here are the following files, in detail, after making some changes to sensitive information.

views.py:

@csrf_exempt
def create_checkout_session(request):
    if request.method == 'POST':
        try:
            checkout_session = stripe.checkout.Session.create(
                line_items=[
                    {
                        'price': 'pr_123456789',
                        'quantity': 1,
                    },
                ],
                mode='payment',
                success_url=DOMAIN + 'payments/success',
                cancel_url=DOMAIN + 'payments/cancel',
                automatic_tax={'enabled': True},
            )
        except Exception as e:
            return HttpResponse(f'<h1>{str(e)}</h1>')

        return redirect(checkout_session.url)

urls.py:

urlpatterns = [
    path('', views.home, name='home'),
    path('checkout/', views.create_checkout_session, name='checkout'),
    path('success/', views.success, name='success'),
    path('cancel/', views.cancel, name='cancel'),
]

The checkout_session variable takes arguments and returns an object where I can call checkout_session.url as seen in views.py.

I am under the impression that checkout_session.url is an External URL due to the fact that Stripe generates and hosts this link. To confirm this, I ran a console print on checkout_session.url and it returned this:

checkout_session.url

https://checkout.stripe.com/c/pay/cs_test_long_hex_variable_relevant_to_my_API_keys

This URL is unique for every checkout session generated by Stripe.

I am certain that stripe has it's own security but what I am concerned about is the 'Cross-Site' Part of CSRF Protection.

  • Is it my responsibility to maintain security for a user to an external payment provider?
  • Is this even how CSRF protection works?
  • If so, what is Django's built in workaround, because this must be a common practice?

Resources used:

What I've Tried:

I have attempted to find similar solutions, I have found multiple questions here that relate to my problem but none that I've found cover the specifics of CSRF Protection over external urls.

Django Documentation states that using the {% csrf_token %} during POST to an external url is forbidden, but after further investigation, does not elaborate on any workaround or alternatives to this.

After coming to the conclusion that the solution is not obvious, I am under the impression that my problem is caused by none other than myself, due to my lack of knowledge on CSRF protection, and Web security in general. Which brings me here to the pool of knowledge that is StackOverflow.

roiul
  • 23
  • 5
  • 1
    What is the URL named `'checkout'`? Please show the apprpriate lines from your `urls.py` file that defines this. – Code-Apprentice Jun 24 '23 at 16:46
  • 1
    From what I can see of your code here, you are not posting the form directly to an external stripe URL. It looks like your view has the code which interacts with stripe. You will need to maintain the CSRF token to post to your own endpoint. – Code-Apprentice Jun 24 '23 at 16:47
  • 1
    If my answer below doesn't answer your question, please [edit] your question to show your `urls.py` file and the entire `create_checkout_session()` view function. I am guessing what the details are in these in order to answer your question and it would be better if I didn't have to guess. – Code-Apprentice Jun 24 '23 at 16:57
  • @Code-Apprentice Updated to show the full details, hopefully that helps, apologies for the guess work. To confirm, the 'checkout' URL is linked to my view function which generates a URL using Stripe's API. – roiul Jun 26 '23 at 00:51
  • 1
    AFAICT, my guesses were correct. The form is NOT posting to an external URL as you are worried about, so you DO need the CSRF token. If you want more help, you should provide links to the exact section of the documentation you are referring to and quote from it directly so that we can clear up any confusion of the wording. – Code-Apprentice Jun 26 '23 at 04:13
  • 1
    You are correct that `checkout_session.url` is an external URL, but you are not posting to it. I am guessing that `redirect()` will do a GET request. You can easily confirm this by checking the Network tab in your browser. – Code-Apprentice Jun 26 '23 at 04:23

2 Answers2

1

You actually don't need to use the csrf token, because it's an external site with it's own security, I don't see how it can affect you by not using it. Stripe has it's own protection and will give you a webhook request to confirm everything went fine, no need of using csrf token then.

L1Lbg
  • 21
  • 1
  • 4
  • My concern is that the 'checkout' URL is required to be @csrf_exempt on my end. If I have any authentication issues, or issues with stripes API in the future. The user will still land on a non-CSRF protected URL before being redirected to a safe one. – roiul Jun 26 '23 at 00:59
1

I assume that you have something like this in urls.py:

urlpatterns = [
  // ...
  path('checkout', views.create_checkout_session, name='checkout'),
]

So when you have a form in your template like this:

<form rel="noreferrer" action="{% url 'checkout' %}" method="POST">

Clicking the submit button will post to your view, not some external URL. This means that you should keep the CSRF token as you have it and don't use the @csrf_exempt decorator.

On the other hand, if your form used a stripe URL directly like this:

<form rel="noreferrer" action="https://stripe.com/api/checkout" method="POST">

Then you don't want to include the CSRF token because the form would send it directly to the external URL.

In your case, the CSRF token is sent back to your own view, so there is no security concern. The view then has code to send a request to stripe. Just be sure you don't include the CSRF token in that code anywhere.

Code-Apprentice
  • 81,660
  • 23
  • 145
  • 268
  • 1
    I will be including the csrf_token in my POST request to 'checkout', It makes a lot more sense to carry the token until the user is redirected, thank you for breaking that down for me. – roiul Jun 27 '23 at 12:04