Hello guys, after a long time, I decided to write a blog post. I chose a vulnerability that I recently uncovered in Hashnode. As you may have already noticed, I set up this blog on Hashnode. Naturally, when I use a third-party service like this, I spend a few hours checking their security as a concerned customer.
The first eye-catching feature in Hashnode is cross-domain authentication. Personally, when I hunt, I always look for authentication class vulnerabilities, which I’m stronger in, especially when an authentication token is transferring among different places. Hashnode has an option for blog owners to have their own domain, as you are reading this post on blog.voorivex.team
and not the Hashnode website.
It's not a big deal. By setting a simple cname
record in the DNS server, you can verify that the domain belongs to the user, and the traffic will be redirected to Hashnode servers:
From a backend perspective, everything is the same, except for the host
header of the HTTP packet. Since the IP address is the same for all of Hashnode’s blogs, Hashnode uses the host
header to determine which blog should be loaded. However, from a browser's perspective, the URLs are different. We all know that browsers store data based on the Origin
:
So, if a user enters credentials and logs into the Hashnode website, they should repeat the procedure to log into blog.voorivex.team
too. To be user-friendly, Hashnode uses an authentication transfer, which means when somebody has an authentication session on the Hashnode website, they will automatically get an authentication session on other CNAMed domains too. But how?
Cross-Domain Authentication
The user opens hashnode.com/authenticate equipped with JWT authentication Cookie. The HTTP request has a parameter named next which is responsible for redirecting the user back:
GET /authenticate?next=https%3A%2F%2Fblog.voorivex.team%2F HTTP/1.1 Host: hashnode.com Cookie: redacted User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Referer: https://blog.voorivex.team/ Upgrade-Insecure-Requests: 1 Sec-Fetch-User: ?1 Te: trailers Connection: close
The response:
HTTP/2 307 Temporary Redirect Age: 0 Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate Content-Security-Policy: default-src *; script-src * 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; font-src *; img-src * data: Date: Mon, 15 Apr 2024 15:25:10 GMT Location: https://hashnode.dev/identity?guid=f4e4e11f-4c6f-43b7-ab41-6483c52a0b4d&next=https%3A%2F%2Fblog.voorivex.team%2F Referrer-Policy: origin-when-cross-origin Server: Vercel Strict-Transport-Security: max-age=63072000 X-Content-Type-Options: nosniff X-Frame-Options: deny X-Matched-Path: /authenticate X-Vercel-Cache: MISS X-Vercel-Id: fra1::pdx1::pldpk-1713194710837-255be6ebcd6a Content-Length: 112 https://hashnode.dev/identity?guid=f4e4e11f-4c6f-43b7-ab41-6483c52a0b4d&next=https%3A%2F%2Fblog.voorivex.team%2F
There is an intermediate phase here which Hashnode checks token and next URL before redirecting the user back:
GET /identity?guid=f4e4e11f-4c6f-43b7-ab41-6483c52a0b4d&next=https%3A%2F%2Fblog.voorivex.team%2F HTTP/1.1 Host: hashnode.dev Cookie: redacted User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Referer: https://blog.voorivex.team/ Upgrade-Insecure-Requests: 1 Sec-Fetch-User: ?1 Te: trailers Connection: close
If the user has a valid JWT authentication Cookie and legitimate next URL value, they will be redirected back to the value of next parameter (in this case, blog.voorivex.team which is legitimate in Hashnode)
HTTP/2 307 Temporary Redirect Age: 0 Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate Content-Security-Policy: default-src *; script-src * 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; font-src *; img-src * data: Date: Mon, 15 Apr 2024 15:25:11 GMT Location: https://blog.voorivex.team/identity?guid=f4e4e11f-4c6f-43b7-ab41-6483c52a0b4d&next=https://blog.voorivex.team/ Referrer-Policy: origin-when-cross-origin Server: Vercel Set-Cookie: redacted Max-Age=315360000; Domain=.hashnode.dev; HttpOnly Strict-Transport-Security: max-age=63072000 X-Content-Type-Options: nosniff X-Frame-Options: deny X-Matched-Path: /identity X-Vercel-Cache: MISS X-Vercel-Id: fra1::pdx1::8tmnc-1713194711393-f85cf186df76 Content-Length: 110 https://blog.voorivex.team/identity?guid=f4e4e11f-4c6f-43b7-ab41-6483c52a0b4d&next=https://blog.voorivex.team/
The user opens blog.voorivex.team with an Authentication Token and if it is a valid token, they will be provided with a JWT authentication Cookie
Here is the flow of cross-domain authentication:
DNS Rebinding
As observed, in the final stage of the flow, if the token is valid, the user will receive a JWT authentication Cookie. If the next URL can be manipulated, the GUID token can be stolen, leading to an account takeover. Although the checker function in the backend is quite secure, let's address this question: how does Hashnode define legitimate domains? How does the next_check() function operate? Upon investigation, it was found that Hashnode verifies the next URL against its database, which includes lists of custom domains. Therefore, an attacker could bind a legitimate domain to their account to manipulate the whitelist. Here's the attacking scenario via DNS Rebinding:
The attacker binds a legitimate domain pointing to hashnode.network, in this case https://attacker.voorivex.team
Since this URL is considered legitimate, the next URL can also be set as https://attacker.voorivex.team
The attacker then changes the DNS value of attacker.voorivex.team to their own IP address (Before launching the attack, they obtain a valid SSL certificate)
Despite changing the DNS records, the next URL remains valid as https://attacker.voorivex.team. It may take a significant amount of time for Hashnode to update its database. Since the attacker does not remove their domain from the whitelist, just manipulates DNS records, the domain remains considered legitimate in Hashnode's system
The attacker provides the victim with a link: https://hashnode.com/authenticate?next=https%3A%2F%2Fattacker.voorivex.team%2F. If the victim clicks on this link, their GUID token will be stolen. The attacker can then produce a JWT using the stolen GUID token, gaining unauthorized access to the victim's account
DNS Rebinding
I'm not going to explain the DNS rebinding vulnerability here; a quick search will give you plenty of information. The important questions for me were:
How is the
next
parameter value validated in the backend? Can the value be anything, or does it just match against the database? What if I change the DNS records of the verified domain?
As observed, in the final stage of the flow, if the token is valid, the user will receive a JWT authentication Cookie. If the next URL can be manipulated, the GUID token can be stolen, leading to an account takeover. Although the checker function in the backend is quite secure, let's address this question: how does Hashnode define legitimate domains? How does the next_check()
function operate? Upon investigation, it was found that Hashnode verifies the next URL against its database, which includes lists of custom domains. Therefore, an attacker could bind a legitimate domain to their account to manipulate the whitelist. Here's the attacking scenario via DNS Rebinding:
The attacker binds a legitimate domain pointing to hashnode.network, in this case
https://attacker.voorivex.team
Since this URL is considered legitimate, the next URL can also be set as
https://attacker.voorivex.team
The attacker then changes the DNS value of
attacker.voorivex.team
to their own IP address (Before launching the attack, they obtain a valid SSL certificate)Despite changing the DNS records, the next URL remains valid as
https://attacker.voorivex.team
. It may take a significant amount of time for Hashnode to update its database (or DNS cache). Since the attacker does not remove their domain from the whitelist, just manipulates DNS records, the domain remains considered legitimate in Hashnode's system
The attacker provides the victim with a link: https://hashnode.com/authenticate?next=https%3A%2F%2Fattacker.voorivex.team%2F
. If the victim clicks on this link, their GUID token will be stolen. The attacker can then produce a JWT using the stolen GUID token, gaining unauthorized access to the victim's account.
Automation
I wrote a Python script to automatically change the DNS record and then open the malicious URL on behalf of the victim. This ensures that the attack will work every time if the victim opens the attacker’s website. In terms of CVSS calculation, the attack complexity is low, making the overall vulnerability score 8.0 (High). The script:
Responsible Disclosure
Immediately after finding the flaw, I reported it to Hashnode. There is no obvious BBP for Hashnode, but a brief documentation worked for me. After about 20 days, they patched the vulnerability and issued a bounty to me.