# Breaking GitHub Private Pages for $35k

CRLF injection in GitHub Pages page_id param plus cookie-prefix case bypass and cache poisoning yields XSS on private github.io pages, pivoting to org-private pages.

- Payout: $35,000
- Severity: high
- Program: GitHub
- Credited: Robert Chen, Philip
- Disclosed: 2021-04-04
- Write-up quality: 9/10
- Original: https://robertchen.cc/blog/2021/04/03/github-pages-xss
- Library page: https://bugbountylibrary.com/r/blog%3Arobertchen-cc-blog-2021-04-03-github-pages-xss
- Tags: domain:web impact:ato impact:integrity kind:bounty platform:blog sev:high tech:nginx technique:parser-differential technique:source-review vuln:cache-poisoning vuln:crlf-injection vuln:xss

**Target:** GitHub Pages private-page auth endpoint /__/auth on *.github.io
**Root cause:** page_id is parsed as an integer for lookup but also rendered verbatim into the Set-Cookie header; a null byte terminates integer parsing while CRLFs break out of the header, and the server matches cookie prefixes case-insensitively.
**Steps:**
1. Observe `page_id=12345%20` yields `Set-Cookie: __Host-gh_pages_id=12345 ; ...` — value reflected into header.
2. Send `page_id=12345%0d%0a%0d%0a` to confirm CRLF injection breaks headers.
3. Append null byte to end integer parse: `page_id=` + encodeURIComponent("\r\n\r\n\x00<script>alert(origin)</script>") → XSS.
4. Bypass nonce: set `__HOST-gh_pages_session=<nonce>; domain=.private-org.github.io` from sibling page (server ignores prefix capitalization).
5. Redirect victim to `https://github.com/pages/auth?nonce=<nonce>&page_id=<payload>&path=Lw`.
6. Poison cache keyed on parsed integer page_id so payload serves to other users of that page.
**How found:** While mapping the custom auth flow, noticed page_id whitespace was ignored yet reflected into Set-Cookie, hypothesized int-parse vs raw-render split; knowing the server was openresty nginx, tried null byte to terminate integer parsing while keeping CRLF header breakout. Nonce bypass came from reading cookie RFCs and testing __HOST- vs __Host- capitalization handling.
**Exploitation:** Attacker with read access to any page in an org sets a fake nonce via sibling subdomain cookie (case-insensitive __Host- bypass), then lures victim through /pages/auth with CRLF+null-byte page_id payload; cached response delivers XSS on the private page domain, and cookies set on shared org.github.io parent pivot to other private pages.
**Escalation:** Cache poisoning on parsed-integer page_id key escalates from user-interaction XSS to persistent XSS against non-interacting users; public-private pages (public repo with private page) let an outside account pivot to internal private pages for the $15k bonus.
**Insight:** When a parameter is both parsed (e.g. to_int) and reflected, look for parser-differential terminators like null bytes that satisfy one path while letting the other emit raw bytes; also test cookie __Host-/__Secure- prefix case sensitivity server-side, since browsers and servers may disagree.
