Security Audit Report

GitHub API Security Audit: 5 Findings, HEAD Returns 200 Where GET Returned 403

https://api.github.com
90 A Good security posture
5 findings
1 Critical 1 High 3 Low
Share
01

About This API

GitHub's REST API at https://api.github.com is, by raw consumer count, the most-consumed developer API on the open web. Every package manager that resolves a GitHub-hosted dependency hits it. Every CI runner that clones a repo hits it. Every gh CLI invocation, every Dependabot alert, every Renovate update, every IDE GitHub-integration plugin, every code-search tool, every supply-chain scanner — all of them issue requests against this base URL.

The root endpoint https://api.github.com/ is documented as the API's catalog: an authenticated or anonymous GET returns a JSON object whose values are URL templates pointing at every resource the API exposes — current_user_url, repository_url, search_users_url, organization_url, and a few dozen others. It is the one URL on the API that is genuinely meant to be world-readable, version-stable, and machine-discoverable. It is also the URL where a client tooling library typically begins its config.

This audit scans only that root endpoint. We did not authenticate, we did not walk into /repos/, /users/, /orgs/, or any of the surfaces where GitHub's interesting authorization logic actually lives. Every finding in this report is about the front door — the response shape and headers of GET / on api.github.com. That scope limitation matters and we are upfront about it: a scan of api.github.com's root cannot say anything about how GitHub handles fine-grained personal access tokens, how it scopes OAuth app permissions, how it gates the GraphQL surface at api.github.com/graphql, or how it enforces the scopes that 100+ million developer accounts depend on. Those surfaces deserve their own audits and would require authenticated scanning that this case study did not perform.

What this audit can do is tell you what the front door looks like — and at GitHub's scale, the front door is itself an artifact worth studying. Whatever api.github.com/ emits in its response headers ripples through how millions of API clients expect responses to look, because GitHub's API is the reference implementation that countless other API designers consult before choosing a versioning scheme, a CORS policy, or a rate-limit-header convention.

02

Threat Model

The threat model for the api.github.com root endpoint is narrow. The endpoint returns a discovery catalog of URL templates — no PII, no repository data, no token, no user. An attacker compromising the response of the root would get a list of strings the rest of the API already publishes in its documentation. There is no productive direct attack on this URL.

The propagation surface

What is interesting about this audit is not the threat to GitHub but the threat to everything downstream of GitHub's example. Three response patterns at the root endpoint propagate, by sheer reference-implementation gravity, into the rest of the developer-API ecosystem.

Pattern 1: header-based versioning. GitHub's response carries X-GitHub-Api-Version-Selected: 2022-11-28. There is no /v1/ in the URL. GitHub's chosen versioning scheme — content negotiation via the Accept header for the legacy v3 surface, plus the dated X-GitHub-Api-Version header for the modern surface — is a deliberate, well-considered choice that suits an API maintained by a team with a strong deprecation policy. The scanner correctly observes the absence of URL-path versioning and flags it. The interesting question for API designers reading this is whether they should copy GitHub's choice. GitHub has the operational discipline to maintain header-based versioning across a 15-year API lifecycle. Most API teams do not. The pattern works at GitHub specifically because GitHub maintains it specifically.

Pattern 2: wildcard CORS on public read-only catalogs. GitHub's root emits Access-Control-Allow-Origin: *. This is correct for the unauthenticated public catalog: any browser on any origin should be able to discover the API surface. The propagated risk is the same as on every other public-mock audit in this series — students and engineers see the wildcard in GitHub's response, conclude that wildcard CORS is a defensible default, and copy it into their own backends without distinguishing public-catalog endpoints from authenticated-resource endpoints. GitHub itself draws that distinction internally; the wildcard is on the public surface, and authenticated requests with cookies don't combine with the wildcard in a way browsers will permit. Imitators rarely retain the distinction.

Pattern 3: rate-limit headers on every response. GitHub emits X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, and X-RateLimit-Reset on the root response. This is the gold standard. The scanner did not flag rate-limit headers as missing because they are present. Every other public API in this case study series got dinged on this; GitHub gets it right. The propagated lesson here is positive: this is the example to follow.

The HEAD versus GET asymmetry

The CRITICAL finding — HEAD returning 200 where GET returned 403 in the scanner's run — is real in the trace but almost certainly a scanner-side or transient anomaly at this URL. Public reproduction with a default User-Agent shows GET returning 200 with the catalog payload. The most likely explanations are that the scanner's GET request was sent without a User-Agent header (GitHub's API requires one and returns 403 when it is absent), while the HEAD probe carried a default agent or was handled by an edge layer that did not enforce the User-Agent check. Either way, the asymmetry is visible in the trace and worth explaining rather than dismissing.

03

Methodology

middleBrick ran a black-box scan against https://api.github.com — the API root, not any sub-resource. No authentication header was sent. No probe payloads beyond the standard multi-method sweep. The scan covered fourteen security-check categories. Five produced findings; nine produced clean negatives.

The five findings:

  • CRITICAL: HEAD returns 200 where GET (in the scanner's trace) returned 403 — same path, different methods, different status (authentication category)
  • HIGH: Access-Control-Allow-Origin: * on the response (inputValidation category, structural to a public catalog)
  • LOW: missing Cache-Control finding (1-of-4) — the response actually carries cache-control: public, max-age=60, s-maxage=60 on a public reproduction; the LOW flag relates to the heuristic's exact-match expectation
  • LOW: OPTIONS preflight advertises Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE (inputValidation category — accurate; the API genuinely accepts those methods on the appropriate authenticated paths)
  • LOW: no URL-path versioning (inventoryManagement — GitHub versions via headers; the heuristic flags any URL without /v1/-style prefixes)

The nine clean negatives are the more interesting half of the scan. GitHub did not trigger:

  • BOLA / IDOR findings (the root response contains URL templates, not numeric IDs)
  • BFLA findings (the discovered methods route to authenticated endpoints, not directly to the root)
  • Property-authorization findings (no sensitive-shaped fields in the catalog response)
  • Resource-consumption findings (rate-limit headers are emitted on every response, including the unauthenticated probe)
  • Data-exposure findings (no password, token, email, or credential patterns in the catalog body)
  • Encryption findings (HSTS is present at max-age=31536000; includeSubdomains; preload — the maximum reasonable configuration)
  • Unsafe-consumption findings (no embedded redirect URLs that consume external state)
  • SSRF findings (the catalog response is a fixed string list)
  • LLM, Web3, and DeFi-specific findings (not applicable; GitHub's API is a Git-and-issues surface, not an inference or chain-RPC surface)

The category-score map confirms the picture: 13 of 14 categories scored at or near 100. Only authentication (45) and inputValidation (65) dropped, and both drops trace to specific findings discussed below.

04

Results Overview

GitHub API scored 90/100 — an A grade — on five findings against the root endpoint https://api.github.com. Distribution: 1 CRITICAL, 1 HIGH, 0 MEDIUM, 3 LOW.

The CRITICAL is the HEAD/GET method asymmetry. The HIGH is the wildcard CORS, structural to a public catalog. The three LOWs are the missing-Cache-Control directive (a heuristic mismatch, since the actual response does carry Cache-Control), the OPTIONS-advertised mutating methods (accurate-but-flagged), and the no-URL-path-versioning finding (GitHub uses header-based versioning instead).

For comparison, recent public-API case studies in this series produced these totals:

  • SWAPI: 4 findings, score 91 (A) — current cleanest result
  • GitHub API: 5 findings, score 90 (A)
  • HTTPBin: 11 findings, score 82 (B)
  • Random User Generator: 12 findings, score 79 (B)
  • Rick and Morty API: 9 findings, score 78 (B)
  • PokéAPI: 12 findings, score 76 (B)
  • FakeStoreAPI: 10 findings, score 75 (C)

GitHub joins SWAPI in the A bucket. The two scans are stylistically different — SWAPI's score comes from a tiny response shape with no operator surface, GitHub's comes from disciplined header conventions on a much larger and more consequential API — but both demonstrate the same underlying principle. Findings count drops when the response body and headers are designed deliberately rather than emitted by framework defaults.

One note about the score floor. We only scanned the root endpoint of api.github.com. A scan of /repos/{owner}/{repo} with a token would necessarily produce a different finding set, both because the surface is larger and because the authorization logic is testable. This score is not a comprehensive grade for GitHub's API security posture — it is a snapshot of the front door.

05

Detailed Findings

Critical Issues 1
CRITICAL

Auth bypass via HTTP method: HEAD returns 200

The endpoint requires authentication for GET (403) but HEAD returns 200 without credentials.

Remediation

Enforce authentication consistently across all HTTP methods. Configure your framework to require auth on the route level, not per-method.

authentication
High Severity 1
HIGH CWE-942

CORS allows all origins (wildcard *)

Access-Control-Allow-Origin is set to *, allowing any website to make requests.

Remediation

Restrict CORS to specific trusted origins. Avoid wildcard in production.

inputValidation
Low Severity 3
LOW CWE-693

Missing security headers (1/4)

Missing: Cache-Control — sensitive response caching.

Remediation

Add the following headers to all API responses: cache-control.

authentication
LOW CWE-650

Dangerous HTTP methods allowed: DELETE, PUT, PATCH

The server advertises support for methods that can modify or delete resources.

Remediation

Only expose HTTP methods that are actually needed. Disable TRACE, and restrict DELETE/PUT/PATCH.

inputValidation
LOW CWE-1059

No API versioning detected

The API URL doesn't include a version prefix (e.g., /v1/) and no version header is present.

Remediation

Implement API versioning via URL path (/v1/), header (API-Version), or query parameter.

inventoryManagement
06

Attacker Perspective

An attacker has nothing productive to do against the api.github.com root. The catalog response is a static URL-template list that is documented and world-readable by design. The interesting attacker exercise here is not against GitHub but in studying how GitHub's example shapes the rest of the developer-API ecosystem — and how an attacker exploits the imitations.

The response shape that gets copied

API designers cargo-cult GitHub. Every internal API design review that has ever happened in a tech company surfaces the question "how does GitHub do it?" within the first ten minutes. That cultural gravity means decisions GitHub made carefully — header-based versioning, hypermedia-style URL templates in the root, rate-limit headers on every response, scoped tokens with explicit permissions — get copied into APIs whose maintainers do not have the operational maturity to support those decisions. The attacker's productive search is not against GitHub but for cargo-culted implementations: APIs that adopted the surface conventions but lack the discipline behind them.

Header-based versioning without the deprecation discipline

If you find an API that uses X-Api-Version-style headers but does not publish a deprecation calendar, does not return Sunset headers, does not version its Accept media types, and does not have a /api-deprecations page, you have found an API whose maintainers copied GitHub's surface choice without copying its substance. The attack on such an API is not a direct vulnerability but a long-term integration risk — clients pinned to the un-versioned URL will silently start receiving altered response shapes when the maintainer changes the schema, often without explicit notice.

Wildcard CORS on a non-public surface

The classic mis-imitation of GitHub's CORS posture is to emit Access-Control-Allow-Origin: * on every response, including authenticated responses. GitHub does not do this — credentialed requests against authenticated endpoints get a more carefully scoped CORS posture. The imitator who skips that scoping ships a CORS surface that lets any third-party origin read authenticated responses if the user's browser holds session credentials for the API's domain. That is a real cross-origin information-disclosure surface, distinct from anything in this audit, and routinely found in APIs whose authors copied GitHub's catalog response wholesale into their own root.

07

Analysis

The CRITICAL HEAD-vs-GET finding is the technically interesting one and worth walking through carefully. The trace recorded by the scanner is:

HEAD https://api.github.com/  →  200
GET  https://api.github.com/  →  403

That asymmetry is real in the trace but does not reproduce against a default-User-Agent client. A direct curl run from a normal terminal shows:

$ curl -sI https://api.github.com
HTTP/2 200
cache-control: public, max-age=60, s-maxage=60
x-github-api-version-selected: 2022-11-28
access-control-allow-origin: *
strict-transport-security: max-age=31536000; includeSubdomains; preload
server: github.com
content-type: application/json; charset=utf-8
x-ratelimit-limit: 60
x-ratelimit-remaining: 59
...

Both HEAD and GET return 200 with this client. The most plausible explanation for the scanner's 403 on GET is the way GitHub's API enforces the User-Agent requirement. The published documentation states: "All API requests MUST include a valid User-Agent header. Requests with no User-Agent header will be rejected." If the scanner's GET probe was issued with no User-Agent and the HEAD probe was issued with a default one (or vice versa, or if the User-Agent enforcement is method-asymmetric at GitHub's edge), the trace explains itself. Either way, the asymmetry visible in the scan trace is not an exploitable bypass — it is a side effect of GitHub's User-Agent gating combined with the scanner's probe construction. Worth understanding rather than ignoring.

The CORS finding:

access-control-allow-origin: *

This is correct on the unauthenticated public catalog. Browsers will not combine the wildcard with credentialed requests, so the catalog cannot be used as a vector to read a logged-in user's data through their browser session. The wildcard is also visible in the OPTIONS preflight response, which lists the methods the API accepts on its various sub-resources. The catalog itself does not accept POST / PUT / PATCH / DELETE; those methods are valid on resource paths discovered via the catalog, under authentication.

The Cache-Control LOW deserves a specific note. The actual response carries cache-control: public, max-age=60, s-maxage=60, which is a deliberate, defensible cache policy for a stable catalog. The scanner's heuristic for the missing-security-headers check looks for an exact-match presence of certain directives and flagged a 1-of-4 deficiency on this response. Operationally, GitHub's Cache-Control is well-considered; the heuristic is conservative.

The advertised mutating methods are real. GitHub's API genuinely accepts DELETE, PUT, PATCH, and POST on the appropriate paths under appropriate authentication. The OPTIONS preflight response is honestly describing the API's surface. The flag is structurally accurate; the implication that the surface is dangerous is what gets nuanced.

The no-URL-versioning finding catches GitHub's deliberate choice to version via headers instead. The scanner's heuristic does not currently understand X-GitHub-Api-Version as a versioning mechanism; it only checks the URL path. A future heuristic refinement could detect dated Api-Version-style headers and reduce the false-positive rate on this specific finding.

08

Industry Context

GitHub sits at the apex of the developer-tools API category. It is older than nearly every API its consumers also touch — older than Stripe (2010 vs 2008 for GitHub), older than Twilio's REST API, older than the AWS SDK as it exists today. Among same-era developer APIs, GitHub's posture is in the same league as Stripe's, AWS's, and the GitLab API's. None of those would be expected to produce a high-finding-count black-box scan against their public root.

The propagation effect of GitHub specifically is greater than any single peer. GitHub's API is not just popular — it is structural to the modern software supply chain. npm install from a GitHub-hosted package, a Renovate dependency update, a CodeQL scan in CI, a Dependabot security alert, every gh CLI command — these all hit api.github.com. Any change in the root response or its headers ripples through tooling at scale within hours. GitHub's API team is, in effect, the maintainer of a global API contract, and the maintenance discipline visible in the response (versioning headers, scoped rate-limit headers, conservative cache policy) reflects the seriousness of that role.

Compliance posture: GitHub Enterprise variants are SOC 2 Type II audited, ISO 27001 certified, in scope for various regional data-protection regimes, and operate under GitHub's published Trust Center documentation. None of that is observable from a black-box root scan, but it informs how to read these findings. None of the five findings here would alter the compliance posture; they are surface conventions, not control deficiencies.

OWASP API Top 10 mapping for what this scan reaches:

  • API1 (broken object-level authorization) — not testable from the root
  • API2 (broken authentication) — partially touched by the HEAD/GET asymmetry, though the explanation traces back to User-Agent gating rather than to a broken auth path
  • API3 (broken property-level authorization) — not testable from the root
  • API8 (security misconfiguration) — covers the wildcard CORS and the Cache-Control flag
  • API9 (improper inventory management) — covers the no-URL-versioning finding and the OPTIONS-advertised methods

The remaining categories of the API Top 10 — broken function-level authorization, unrestricted resource consumption, server-side request forgery, unsafe consumption of APIs, unrestricted access to sensitive business flows — would require an authenticated scan against specific resource paths to evaluate, and were not in scope here.

09

Remediation Guide

HEAD/GET asymmetry under specific probe construction

For consumers of the GitHub API: always send a User-Agent header. GitHub documents this as a hard requirement; requests without one are rejected. Send a descriptive value identifying your tool and a contact URL.

// Node.js — fetch with required User-Agent
const res = await fetch('https://api.github.com/', {
  headers: {
    'User-Agent': 'my-tool/1.0 (+https://example.com)',
    'Accept': 'application/vnd.github+json',
    'X-GitHub-Api-Version': '2022-11-28'
  }
});

Wildcard CORS — only-cargo-cult-the-public-catalog-pattern

If you are designing your own API and modeling it on GitHub: emit Access-Control-Allow-Origin: * only on responses that are genuinely public and unauthenticated. On responses that depend on session cookies or bearer tokens, set an explicit allowlist of trusted origins instead.

// Express — distinguish public-catalog from authenticated responses
import cors from 'cors';

// Public catalog — wildcard is acceptable
app.get('/', cors({ origin: '*' }), catalogHandler);

// Authenticated routes — explicit allowlist only
const authedCors = cors({
  origin: ['https://app.example.com', 'https://admin.example.com'],
  credentials: true
});
app.get('/repos/:owner/:repo', authedCors, requireAuth, repoHandler);

Header-based versioning — copy the substance, not just the surface

If you adopt header-based versioning like GitHub's, commit to the operational discipline that makes it work: publish a deprecation calendar, return Sunset and Deprecation response headers when retiring a version, document the policy in machine-readable form. Without this, header-based versioning becomes a quiet contract that breaks integrations.

// Express — emit Deprecation and Sunset headers on a soon-to-be-retired version
app.use((req, res, next) => {
  const version = req.get('X-Api-Version') || '2025-01-01';
  res.set('X-Api-Version-Selected', version);
  if (version === '2024-01-01') {
    res.set('Deprecation', 'true');
    res.set('Sunset', 'Sun, 31 Dec 2026 23:59:59 GMT');
    res.set('Link', '<https://example.com/api-deprecations>; rel="deprecation"');
  }
  next();
});

Rate-limit-header discipline

GitHub emits X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Reset, and X-RateLimit-Resource on every response. Match this on your own API; consumers that build against you will appreciate it and your support load drops.

// Express — emit GitHub-style rate-limit headers
import rateLimit from 'express-rate-limit';

app.use(rateLimit({
  windowMs: 60 * 60 * 1000,
  max: 5000,
  standardHeaders: 'draft-7',
  legacyHeaders: false,
  handler: (req, res) => {
    res.set('X-RateLimit-Resource', 'core');
    res.status(429).json({ message: 'API rate limit exceeded' });
  }
}));

User-Agent enforcement on your own API

If you are designing an API that wants to deter unattributed scrapers: enforce User-Agent presence the same way GitHub does. Reject requests lacking a User-Agent with a clear error message. The cost is small and the support-traceability benefit is real.

// Express — enforce User-Agent presence
app.use((req, res, next) => {
  const ua = req.get('User-Agent');
  if (!ua || ua.trim().length < 3) {
    return res.status(403).json({
      message: 'Request lacks a valid User-Agent header.',
      documentation_url: 'https://example.com/docs/user-agent-required'
    });
  }
  next();
});
10

Defense in Depth

For GitHub specifically, there is essentially no defense-in-depth recommendation. The five findings are either correct-by-design (CORS wildcard on the public catalog, OPTIONS-advertised methods, Cache-Control choice) or scanner-heuristic artifacts (the URL-versioning flag, the HEAD/GET asymmetry under specific probe construction). GitHub's posture is not the action item.

For consumers of the GitHub API — and specifically for engineers building tooling that integrates with it — the meaningful actions are about robustness rather than security:

1. Always send a User-Agent header. The HEAD/GET asymmetry observed in this scan is the empirical consequence of skipping it. Per GitHub's documentation, requests without a User-Agent will be rejected. Send something descriptive: User-Agent: my-tool/1.0 (+https://example.com).

2. Read X-GitHub-Api-Version-Selected on every response and pin via X-GitHub-Api-Version on every request. Header-based versioning works only if both sides participate. Tooling that reads the catalog and then does not pin the version it expects will silently break the day GitHub deprecates a media type.

3. Honor the rate-limit headers. GitHub publishes them on every response. Track X-RateLimit-Remaining and X-RateLimit-Reset; back off proactively when remaining is near zero rather than reacting to 403s and 429s after the fact.

4. Do not cargo-cult GitHub's wildcard-CORS pattern into your own authenticated APIs. The wildcard works for GitHub's public catalog because it is genuinely public and credentials cannot combine with it in a browser context. The same wildcard on an authenticated API is a different posture and a known surface for cross-origin information disclosure.

For API designers using GitHub as a reference: copy the substance, not just the surface. Header-based versioning works at GitHub because GitHub commits to maintaining it for years and publishes deprecation calendars. Rate-limit headers work because the API team treats them as a contract, not a courtesy. The scoped fine-grained personal access token system works because GitHub invested years in building the permission taxonomy. Adopting any of these conventions without the operational discipline behind them produces a fragile imitation, not a robust API.

11

Conclusion

GitHub's REST API root scored 90/100 with five findings — a clean A grade. None of the findings describe an exploitable risk against GitHub itself. The CRITICAL asymmetry traces back to User-Agent gating combined with how the scanner constructed its probes; the HIGH wildcard CORS is correct for a public catalog; the three LOWs are surface-convention flags that GitHub has reasoned about deliberately and chosen against the scanner's defaults.

The interesting half of the scan is the nine clean negatives — rate-limit headers present, HSTS at maximum, no data exposure, no SSRF surface, no resource-consumption finding. GitHub's posture on each of these is what the rest of the developer-API ecosystem points at when asked "how should I do this?" The scan-level evidence supports the cultural reputation.

For consumers, this audit does not change anything actionable. Send a User-Agent. Pin a version. Honor the rate-limit headers. Don't cargo-cult the surface conventions into APIs that lack the discipline behind them.

For maintainers of other developer APIs reading this: the lesson is not that GitHub got lucky. It is that fifteen years of maintenance discipline visibly compounds into the response headers. The shape of api.github.com's root is a long-term artifact, not a default. Aim for the same kind of artifact on your own API and the scanner findings will trend the same way.

Frequently Asked Questions

Did this scan actually find a real vulnerability in GitHub?
No. The CRITICAL HEAD/GET asymmetry visible in the scan trace traces back to GitHub's documented User-Agent requirement combined with how the scanner constructed its probes. Direct reproduction with a normal client returns 200 on both methods. None of the five findings describe an exploitable risk against GitHub itself.
Why only score the root endpoint? Isn't that an unfair scope?
It is a deliberately narrow scope, and we are upfront about it. A scan of api.github.com's root is a snapshot of the front door — the response shape and headers of GET /. We did not authenticate, did not walk into /repos/, /users/, /orgs/, or any of the surfaces where GitHub's interesting authorization logic lives. A full audit of GitHub's API surface would require authenticated scanning across many endpoints and is outside this case study's scope. The score of 90 applies to the root, not to GitHub's full API.
Why is the HEAD/GET asymmetry flagged CRITICAL if it is not exploitable?
Because the scanner's severity assignment is conservative on authentication-related asymmetries. On most APIs, HEAD-returns-different-status-than-GET is a real implementation defect — frequently a middleware-ordering bug — and the scanner flags it accordingly. GitHub's case is unusual: the asymmetry traces back to a documented User-Agent enforcement, not to a middleware bug. The scanner cannot distinguish those two causes from a black-box probe; it flags the symptom and leaves the analysis to humans. We are doing the analysis here.
Should I copy GitHub's API conventions into my own API?
Copy the substance, not just the surface. Header-based versioning is GitHub's right answer because GitHub maintains a deprecation calendar and a media-type contract that has been stable across years. Rate-limit headers are GitHub's right answer because GitHub treats them as a contract. Wildcard CORS on the public catalog is GitHub's right answer because GitHub draws a careful line between public and authenticated responses. Adopting these conventions without the operational discipline behind them produces a fragile imitation.
Is the wildcard CORS on api.github.com a security risk?
Not on the public catalog response, which is the surface this audit covers. Browsers will not combine wildcard CORS with credentialed requests; the catalog cannot be used to read a logged-in user's data through their browser session. The risk that the finding gestures toward is downstream: imitators who copy the wildcard onto their own authenticated APIs without distinguishing public-catalog responses from credentialed responses ship a different, real, cross-origin information-disclosure surface.