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:
- Stripe Documentation
- Django Documentation
- Windows 11
- PyCharm IDE
- Python 3.11
- Django 4.2
- Stripe 5.4
- Firefox 114.0.1
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.