Hacking Better-Hub

Table of Contents
- What is Better-Hub?
- The Vulnerabilities
- 01. Unsanitized README → XSS
- 02. Issue Description → XSS
- 03. Stored XSS in PR Bodies
- 04. Stored XSS in PR Comments
- 05. Reflected XSS via SVG Image Proxy
- 06. Large-File XSS (>200 KB)
- 07. Cache Deception — Private File Access
- 08. Authz Bypass via Issue Cache
- 09. Private Repo Prompt Leak
- 10. GitHub OAuth Token Leaked to Client
- 11. Open Redirect via Query Parameter
- Disclosure Timeline
Vulnerabilities
| # | Vulnerability | Severity |
|---|---|---|
| 01 | Unsanitized README → XSS | 🟠 HIGH |
| 02 | Issue Description → XSS | 🟠 HIGH |
| 03 | Stored XSS in PR Bodies | 🟠 HIGH |
| 04 | Stored XSS in PR Comments | 🟠 HIGH |
| 05 | Reflected XSS via SVG Image Proxy | 🟠 HIGH |
| 06 | Large-File XSS (>200 KB) | 🟠 HIGH |
| 07 | Cache Deception — Private File Access | 🔴 CRITICAL |
| 08 | Authz Bypass via Issue Cache | 🟠 HIGH |
| 09 | Private Repo Prompt Leak | 🟠 HIGH |
| 10 | GitHub OAuth Token Leaked to Client | 🟡 Low (could be High if chained with XSS) |
| 11 | Open Redirect via Query Parameter | 🟡 Low |
What is Better-Hub?
Better-Hub (better-hub.com) is an alternative GitHub frontend — a richer, more opinionated UI layer built on Next.js that sits on top of the GitHub API. It lets developers browse repositories, view issues, pull requests, code blobs, and repository prompts, while authenticating via GitHub OAuth.
Because Better-Hub mirrors GitHub content inside its own origin, any unsanitized rendering of user-controlled data becomes significantly more dangerous than it would be on a static page — it has access to session tokens, OAuth credentials, and the authenticated GitHub API. That attack surface is exactly what I set out to explore.
The Vulnerabilities
01. Unsanitized README Leads to XSS
Description
The repository README is fetched from GitHub, piped through renderMarkdownToHtml with allowDangerousHtml and rehypeRaw enabled — with zero sanitization — then stored in the readmeHtml state and rendered via dangerouslySetInnerHTML in repo-overview.tsx. Because the README is entirely attacker-controlled, any repository owner can embed arbitrary JavaScript that executes in every viewer's browser on better-hub.com.
Steps to Reproduce
- Create a GitHub repository with the following content in
README.md:"><img src=x onerror=prompt(1)>
- View the repository at
https://www.better-hub.com/<user>/<repo>and observe the XSS popup.
Impact
Session hijacking via cookie theft, credential exfiltration, and full client-side code execution in the context of better-hub.com. Chains powerfully with the GitHub OAuth token leak (see vuln #10).
02. Unsanitized Markdown in Issue Description Leads to XSS
Description
Issue descriptions are rendered with the same vulnerable pipeline: renderMarkdownToHtml with raw HTML allowed and no sanitization. The resulting bodyHtml is inserted directly via dangerouslySetInnerHTML inside the IssueConversation thread entry component, meaning a malicious issue body executes arbitrary script for every person who views it on Better-Hub.
Steps to Reproduce
- Create a GitHub issue with the following in the body:
"><img src=x onerror=prompt(1)>
- Navigate to the issue via
https://www.better-hub.com/<user>/<repo>/issues/<num>to trigger the payload.
Impact
Arbitrary JavaScript execution for anyone viewing the issue through Better-Hub. Can be used for session hijacking, phishing overlays, or CSRF-bypass attacks.
03. Stored XSS via Unsafe Markdown Rendering in PR Bodies
Description
Pull request bodies are fetched from GitHub and processed through renderMarkdownToHtml with allowDangerousHtml/rehypeRaw and no sanitization pass, then rendered unsafely. An attacker opening a PR with an HTML payload in the body causes XSS to fire for every viewer of that PR on Better-Hub.
Steps to Reproduce
- Open a pull request whose body contains:
"><img src=x onerror=prompt(5)>
- View the PR through Better-Hub to observe the XSS popup.
Impact
Stored XSS affecting all viewers of the PR. Particularly impactful in collaborative projects where multiple team members review PRs.
04. Stored XSS via Unsafe Markdown Rendering in PR Comments
Description
The same unsanitized renderMarkdownToHtml pipeline applies to PR comments. Any GitHub user who can comment on a PR can inject a stored XSS payload that fires for every Better-Hub viewer of that conversation thread.
Steps to Reproduce
- Post a PR comment containing:
"><img src=x onerror=prompt(8)>
- View the comment thread via Better-Hub to trigger the XSS.
Impact
A single malicious commenter can compromise every reviewer's session on the platform.
05. Reflected XSS via SVG Image Proxy
Description
The /api/github-image endpoint proxies GitHub repository content and determines the Content-Type from the file extension in the path query parameter. For .svg files it sets Content-Type: image/svg+xml and serves the content inline (no Content-Disposition: attachment). An attacker can upload a JavaScript-bearing SVG to any GitHub repo and share a link to the proxy endpoint — the victim's browser executes the script within better-hub.com's origin.
Steps to Reproduce
- Create an SVG file in a public GitHub repo with content:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 124 124"> <script type="text/javascript"> alert(0x539); </script> </svg>
- Direct the victim to:
https://www.better-hub.com/api/github-image?owner=<user>&repo=<repo>&path=xss.svg&ref=main
Impact
Reflected XSS with a shareable, social-engineered URL. No interaction with a real repository page is needed — just clicking a link is sufficient. Easily chained with the OAuth token leak for account takeover.
06. Unescaped Large-File XSS for Files Over 200 KB
Description
When viewing code files larger than 200 KB, the application hits a fallback render path in apps/web/src/components/repo/code-viewer-client.tsx that outputs raw file content via dangerouslySetInnerHTML without any escaping. An attacker can host a file exceeding the 200 KB threshold containing an XSS payload — anyone browsing that file on Better-Hub gets the payload executed.
Steps to Reproduce
- Create a file named
test.htmlcontaining the payload, padded to exceed 200 KB:"><img src=x onerror=prompt(31337)> <!-- pad to >200kb -->
- Browse to the file on Better-Hub at
/blob/main/test.html. The XSS fires immediately.
Impact
Any repository owner can silently weaponize a large file. Because code review is often done on Better-Hub, this creates a highly plausible attack vector against developers reviewing contributions.
07.Authorization Bypass via Shared File Cache
Description
The readLocalFirstGitData function reads file content from a shared Redis cache. Cache entries are keyed by repository path alone — not by requesting user. The file_content field is marked as shareable, so once any authorized user views a private file through the /api/github-image handler or the blob page, its contents are written to Redis under a path-only key. Any subsequent request for the same path — from any user, authenticated or not — is served directly from cache, completely bypassing GitHub's permission checks.
Steps to Reproduce
- Create a private repository and add a file called
secret.txt. - As the repository owner, navigate to the following URL to populate the cache:
https://www.better-hub.com/api/github-image?owner=<user>&repo=<private-repo>&path=secret.txt&ref=main
- Open the same URL in an incognito window or as a completely different user.
- The private file content is served — no authorization required.
Impact
Complete confidentiality breach of private repositories. Any file that has ever been viewed by an authorized user is permanently exposed to unauthenticated requests. This includes source code, secrets in config files, private keys, and any other sensitive repository content.
08. Authorization Bypass via Improper Cache Management on Issues
Description
A similar cache-keying problem affects the issue page. When an authorized user views a private repo issue on Better-Hub, the issue's full content is cached and later embedded in Open Graph meta properties of the page HTML. A user who lacks repository access — and sees the "Unable to load repository" error — can still read the issue content by inspecting the page source, where it leaks in the og:* meta tags served from cache.
Steps to Reproduce
- Create a private repo and create an issue with a sensitive body.
- Open the issue as an authorized user:
https://www.better-hub.com/<user>/<repo>/issues/<num> - Open the same URL in a different session (no repo access).
- While the access-error UI is shown, view the page source — issue details appear in the
<meta property="og:*">tags.
Impact
Private issue contents — potentially including bug reports, credentials in descriptions, or internal discussion — are accessible to any unauthenticated party who knows or guesses the URL.
09. Authorization Bypass — Accessing Prompts on Private Repos
Description
Better-Hub exposes a Prompts feature tied to repositories. For private repositories, the prompt data is included in the server-rendered page source even when the requestor does not have repository access. The error UI correctly shows "Unable to load repository," but the prompt content is already serialized into the HTML delivered to the browser.
Steps to Reproduce
- Create a private repository and create a prompt in it.
- Open the prompt URL as an unauthorized user:
https://www.better-hub.com/<user>/<repo>/prompts/<id>
- View the page source — prompt details are present in the HTML despite the access-denied UI.
Impact
Private AI prompts — which may contain internal instructions, proprietary workflows, or system prompt secrets — leak to unauthenticated users.
10. GitHub OAuth Access Token Leaked to Client-Side JavaScript
Description
getServerSession returns a session object that includes githubUser.accessToken. This session object is passed as props directly to client components (AppNavbar, DashboardContent, etc.). Next.js serializes component props and embeds them in the page HTML for hydration, meaning the raw GitHub access token is present in the page source and accessible to any JavaScript running on the page — including scripts injected via any of the XSS vulnerabilities above.
Root Cause
The fix is straightforward: strip accessToken from the session object before passing it as props to client components. Token usage should remain server-side only.
Impact
When chained with any XSS in this report, an attacker can exfiltrate the victim's GitHub OAuth token and make arbitrary GitHub API calls on their behalf — reading private repos, writing code, managing organizations, and more. This elevates every XSS in this report from session hijacking to full GitHub account takeover.
11. Open Redirect via Unvalidated Query Parameter
Description
The home page redirects authenticated users to the destination specified in the ?redirect= query parameter with no validation or allow-listing. An attacker can craft a login link that silently redirects the victim to an attacker-controlled domain immediately after they authenticate.
Steps to Reproduce
- Log in to Better-Hub with GitHub credentials.
- Navigate to:
https://www.better-hub.com/?redirect=https://example.com
- You are immediately redirected to
example.com.
Impact
Phishing attacks exploiting the trusted better-hub.com domain. Can be combined with OAuth token flows for session fixation attacks, or used to redirect users to convincing fake login pages post-authentication.
Disclosure Timeline
All issues were reported directly to Better-Hub team. The team was responsive and attempted rapid remediation.
| Date | Event |
|---|---|
| Feb 25–26, 2026 | Initial reports sent covering all XSS variants, cache deception, authorization bypasses, OAuth token leak, and open redirect. |
| Feb 26, 2026 — 8:15 PM | Bereket confirmed receipt and stated "everything should be fixed." Mentioned plans to launch a bug bounty program. |
| Feb 26, 2026 | Re-tested cache deception attack — still reproducible. Reported ongoing issue. |
| Feb 27, 2026 — 11:40 PM | Bereket stated the cache issue "should have been fixed as of yesterday." |
| Pending | Awaiting CVE assignment and public advisory on the Better-Hub repository. |