Security Audit Report

CoinGecko Security Audit: 5 Findings, Including No Rate-Limit Headers on the Most Rate-Limited Free Tier in Crypto

https://api.coingecko.com/api/v3/ping
88 B Good security posture
5 findings
1 Critical 2 High 2 Low
Share
01

About This API

CoinGecko at api.coingecko.com is the largest free, no-API-key-required cryptocurrency market data API on the open web. It tracks roughly 14,000 coins across more than 1,000 exchanges and serves price, volume, market-cap, and historical chart data over a plain REST surface. The /api/v3/ping endpoint we scanned is the canonical health-check — it returns {"gecko_says": "(V3) To the Moon!"} and is the endpoint nearly every CoinGecko client library calls first to confirm the service is reachable.

Inside the indie-developer crypto ecosystem, CoinGecko is the API. It's the data source for hobbyist portfolio trackers on every app store. It's the backend behind countless 'I built a crypto dashboard in React' YouTube tutorials. It's the price feed for Discord bots, Telegram bots, and Slack integrations that report token prices into chat channels. It's what students reach for when they want to build their first 'real-data' app and don't want to deal with API-key sign-up flows. The free public tier is what makes that possible.

CoinGecko also operates a separate paid commercial tier at pro-api.coingecko.com (different host, different security posture, different rate limits, requires an API key in the x-cg-pro-api-key header). This audit is exclusively on the free public API. The Pro endpoint has a different threat model and is not in scope here. If you're building anything with a real SLA against CoinGecko, you should be on the Pro endpoint, and you should re-run this audit yourself against that host.

This case study is written for the indie developer who is building or maintaining a portfolio tracker, a price-alert bot, or a crypto dashboard against CoinGecko's free tier — the audience that lives with the rate-limit reality every day.

02

Threat Model

CoinGecko's threat model on the free public tier is uncomplicated. The data is public market data — prices, volumes, market caps that are aggregated from public exchange APIs and republished. There is no PII in the responses, no authenticated user state, no transactional capability. An attacker compromising the API surface yields nothing of value beyond what is already on the exchange APIs themselves. The operational threat is denial-of-service against CoinGecko's infrastructure, which CoinGecko addresses with aggressive per-IP rate limiting (the same rate limiting that produces this audit's most interesting finding).

The propagated rate-limit-handling pattern

The interesting threat surface — as with every public-API audit in this series — is what patterns CoinGecko teaches the developers consuming it. The biggest one is rate-limit handling. Because the API does not emit X-RateLimit-* headers, every client library and every hand-rolled tracker has to invent its own backoff strategy. The strategies range from sensible (exponential backoff on 429, with a hard cap and jitter) to dangerous (retry immediately on 429, which is how CoinGecko users get IP-banned on shared cloud egress).

The propagated CORS pattern

The wildcard Access-Control-Allow-Origin: * is what makes CoinGecko callable from any localhost or production frontend without a backend proxy. It is correct for a public price-data API that wants to be maximally easy to consume. The risk to consumers is when they copy the wildcard CORS pattern into their own backend that holds user portfolio data — the wildcard works fine for fetching prices and is wrong for fetching a user's holdings list.

The propagated 'no auth' pattern

The CRITICAL no-authentication finding is structural to the free public tier and intentional. The risk to consumers is again the propagation: students who build their first crypto app against CoinGecko learn 'fetch JSON, render UI' as a complete pattern, without ever encountering the API-key handling, key rotation, or rate-limit-by-key-tier flows that any real production tracker needs.

What's not in the threat model

No PII exposure (responses are public market data only). No authentication-bypass surface (there is no authentication to bypass on the free tier). No injection surface flagged by the scan (the /ping endpoint takes no parameters). No SSRF, no IDOR confirmation on this endpoint (there are no IDs in the response).

03

Methodology

middleBrick ran a black-box scan against https://api.coingecko.com/api/v3/ping. The scan was read-only — no destructive HTTP methods, no authentication headers, no probe payloads beyond the standard multi-method discovery sweep. The scope is deliberately narrow: this audit covers the /ping endpoint specifically, not the full /api/v3 surface. Endpoints like /coins/{id}/market_chart, /simple/price, /exchanges, and /coins/markets are likely to surface different findings (in particular, those with query parameters would also be exercised against input-validation heuristics). Treat the score and findings here as a posture indicator for the public surface, not a complete inventory of CoinGecko's attack surface.

Fourteen security checks ran across OWASP API Top 10 categories: authentication, BOLA, BFLA, property-level authorization, input validation, CORS, rate limiting, data exposure, encryption, inventory management, unsafe consumption, SSRF, LLM-specific probes, and the Web3 / DeFi check sets. Five checks produced findings; nine produced clean negatives.

The five findings:

  • CRITICAL: API accessible without authentication (authentication category, structural to a free public tier)
  • HIGH: CORS allows all origins (inputValidation category, CWE-942)
  • HIGH: Missing rate-limit headers (resourceConsumption category, CWE-770) — the interesting one
  • LOW: Dangerous HTTP methods advertised via OPTIONS (inputValidation, CWE-650)
  • LOW: HSTS max-age below recommended (encryption category, hygiene)

The Web3 / DeFi check sets ran but produced no findings — correct outcome. CoinGecko is a market-data REST API, not an EVM-RPC or JSON-RPC node. Dynamic category weighting collapsed those categories to their floor weighting since the protocol fingerprint is plain HTTPS REST.

04

Results Overview

CoinGecko received a B grade with a score of 88. Five findings: one critical, two high, zero medium, two low.

The CRITICAL finding (no authentication on GET) is structural. CoinGecko's free tier exists specifically to be called without an API key, from any client. The finding stays critical because the heuristic doesn't know which APIs are intended-public — the scanner correctly raises the signal so it can be triaged by a human reading the report.

The two HIGH findings are (1) wildcard CORS (Access-Control-Allow-Origin: *), again structural to a public read-only data API, and (2) missing rate-limit headers — no X-RateLimit-Limit, no X-RateLimit-Remaining, no X-RateLimit-Reset, no Retry-After. The second one is the finding worth dwelling on.

The two LOW findings are cosmetic in this context: (3) the OPTIONS preflight advertises DELETE / PUT / PATCH (the /ping endpoint doesn't actually accept them; this is the framework default), and (4) HSTS max-age is set to 15,724,800 seconds (~26 weeks) instead of the recommended 31,536,000 (1 year). Both are one-line fixes at the edge.

For comparison, our other public-API case studies in this series have produced these finding totals:

  • SWAPI: 4 findings, score 91 (A) — the cleanest scan
  • CoinGecko: 5 findings, score 88 (B)
  • Rick and Morty API: 9 findings, score 78 (B)
  • FakeStoreAPI: 10 findings, score 75 (C)
  • HTTPBin: 11 findings, score 82 (B)
  • PokéAPI: 12 findings, score 76 (B)
  • Random User Generator: 12 findings, score 79 (B)
  • DummyJSON: 13 findings, score 75 (B)
  • ReqRes: 17 findings, score 73 (C)

CoinGecko sits second-cleanest in this pool. The discipline that produces that result is mostly the same as SWAPI's: a tight, parameter-free response shape (the /ping endpoint returns a 38-byte JSON body) with no internal metadata, no synthetic credentials, and no operator surface exposed.

05

Detailed Findings

Critical Issues 1
CRITICAL CWE-306

API accessible without authentication

The endpoint returned 200 without any authentication credentials.

Remediation

Implement authentication (API key, OAuth 2.0, or JWT) for all API endpoints.

authentication
High Severity 2
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
HIGH CWE-770

Missing rate limiting headers

Response contains no X-RateLimit-* or Retry-After headers. Without rate limiting, the API is vulnerable to resource exhaustion attacks (DoS, brute force, abuse).

Remediation

Implement rate limiting (token bucket, sliding window) and return X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After headers.

resourceConsumption
Low Severity 2
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

HSTS max-age is too short

HSTS max-age is 15724800 seconds (recommended: 31536000 / 1 year).

Remediation

Set Strict-Transport-Security max-age to at least 31536000 (1 year).

encryption
06

Attacker Perspective

An attacker against CoinGecko's free public tier has essentially no productive work. The data is public market data, the surface is read-only, the response on /ping is a fixed string. The interesting role for an attacker — and the one that makes this audit useful — is as a diagnostician of downstream code: the portfolio trackers, price-alert bots, and crypto dashboards that consume the API.

Read the rate-limit-handling code

The most productive attack against tracker apps built on CoinGecko free tier is to read their rate-limit-handling code. Because CoinGecko emits no X-RateLimit-* headers, every client either (a) implements its own per-IP throttle, (b) hopes for the best and retries blindly on 429, or (c) doesn't handle 429 at all and crashes the UI on backoff. Open-source trackers on GitHub overwhelmingly fall into bucket (b) or (c). An attacker looking for portfolio trackers to break can grep for fetch.*coingecko.*\/api\/v3 in public repos, then read the surrounding code for the absence of response.status === 429 handling. The trackers without it are the ones whose front-ends are easy to break by inducing CoinGecko-side rate limits via colocated traffic on the same shared cloud egress IP.

Race the price feed

For trackers that compute portfolio value from a CoinGecko price feed, the propagated risk is stale-price arbitrage. CoinGecko's price endpoints carry no per-coin last_updated timestamp on every product surface (the /simple/price endpoint omits it unless you pass include_last_updated_at=true), so trackers that don't request the timestamp don't know how stale their prices are. On a real exchange or DeFi product priced from a CoinGecko-derived feed, this is the foundation of a stale-oracle exploit. The mitigation is to always request the timestamp and reject prices older than a fixed threshold; the propagated risk is that almost no tracker tutorial mentions it.

Inspect the proxy

Some trackers proxy CoinGecko through their own backend to hide the per-IP rate limit (because the backend's IP gets rate-limited as one client even though many users are behind it). An attacker observing this can hit the proxy directly, exhaust the shared rate budget, and DoS every user of the tracker for the rest of the rate window. The fix is per-user rate limiting at the proxy plus aggressive caching of CoinGecko responses; the propagated risk is that most indie proxies skip both.

07

Analysis

The missing-rate-limit-headers finding is the technically interesting one, because it's structurally correct that the scanner flagged it AND structurally correct that consumers feel it every day. CoinGecko's free tier is widely reported to allow somewhere between 10 and 50 requests per minute per IP, with the actual cap fluctuating over time and not being documented in a stable contract. When you exceed it, you get a 429 response. What you do not get is any header telling you when the budget resets:

$ curl -i https://api.coingecko.com/api/v3/ping
HTTP/2 200
date: ...
content-type: application/json
strict-transport-security: max-age=15724800; includeSubDomains
access-control-allow-origin: *
server: cloudflare

{"gecko_says":"(V3) To the Moon!"}

What's missing from that response is the entire IETF draft-7 set: RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset. Also missing on 429 responses (in our observation, though we did not deliberately exceed the limit during the scan) is Retry-After. The combination forces every client to do one of the bad things listed in the previous section.

The wildcard CORS:

access-control-allow-origin: *

is correct for a public price-data API. It's the entire reason a portfolio tracker can fetch prices client-side from any user's browser without a backend. The wildcard does not allow credentialed requests (cookies, HTTP auth) to be sent, which is the browser-enforced safety mechanism that makes the wildcard tolerable on a public-data API. The pattern is fine on CoinGecko itself; the danger is propagation into authenticated backends that hold user portfolio data.

The dangerous-methods finding:

$ curl -i -X OPTIONS https://api.coingecko.com/api/v3/ping
HTTP/2 204
access-control-allow-methods: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE
...

The advertisement is the framework default, not an indication that any of these methods are accepted on /ping (they aren't — POST/PUT/PATCH/DELETE return 4xx). It's a hygiene finding rather than an exploit surface. The fix is to narrow the advertised method list per route to only the methods the route actually serves.

The HSTS finding:

strict-transport-security: max-age=15724800; includeSubDomains

15,724,800 seconds is approximately 26 weeks. The recommended value is 31,536,000 (one year), which is also the minimum required for HSTS preload-list inclusion. The current value still provides meaningful protection — TLS-stripping requires a fresh client that hasn't seen api.coingecko.com in six months — but on the recommended-config gate it falls short.

The no-authentication finding is the by-design CRITICAL. The endpoint is intended-public, the data is intended-public, the API is intended-public. The scanner doesn't know that, and the report correctly flags it so a human can decide.

08

Industry Context

CoinGecko sits in the public crypto market data API niche alongside CoinMarketCap (paid-only, requires API key for any meaningful endpoint), CryptoCompare (freemium, key-required even on the free tier), Coinpaprika (free-tier with key required for most endpoints), and DefiLlama (free, no-key, focused on DeFi TVL specifically). Among that peer group CoinGecko is the most permissive — the only one with a no-key public tier broad enough to cover the full coin universe — and that permissiveness is exactly what makes it the default choice for indie crypto trackers.

For a market-data API operating at this scale (CoinGecko self-reports more than 800 million monthly API calls), the rate-limit-headers finding is operationally significant. Every consumer that hits 429 has to guess about backoff. The CoinGecko Pro tier at pro-api.coingecko.com does emit rate-limit headers (the documentation is explicit about a per-API-key budget), but the free tier's silence on this is what produces the propagated patterns this audit calls out.

Compliance context for CoinGecko itself is light because the free public surface holds no user data. PCI-DSS, GDPR, LGPD, HIPAA — all out of scope on this endpoint. The Pro tier surface that handles billing and accounts is in scope for those, and a separate audit against pro-api.coingecko.com would be a different document.

Compliance context for tracker apps consuming CoinGecko is heavier than most indie devs realize. A portfolio tracker that holds users' wallet addresses, exchange API keys, or transaction history is in scope for GDPR, CCPA, and (for trackers operating in the EU) MiCA disclosure rules. The fact that CoinGecko itself doesn't carry that load is irrelevant; the tracker that consumes CoinGecko does.

OWASP API Top 10 mapping for what this audit finds: API1 (intentional no-auth), API4 (unrestricted resource consumption — the missing rate-limit headers; CoinGecko enforces the limit but doesn't signal it), API8 (security misconfiguration via wildcard CORS, advertised dangerous methods, short HSTS max-age). Three of ten categories. The remaining seven are not represented because the /ping endpoint's surface is too narrow to produce them — a scan against a parameterized endpoint like /coins/{id}/market_chart would likely surface API3 (BOLA on the {id} path parameter, structural rather than exploitable), API4 (resource consumption on unbounded days ranges), and possibly API6 (mass assignment via undocumented query parameters).

09

Remediation Guide

Missing rate-limit headers (CoinGecko maintainer side)

Emit IETF draft-7 RateLimit-* headers on every response, plus Retry-After on 429. Even an approximate signal is dramatically better than no signal — clients can preemptively slow down instead of reacting to failure.

// Express edge layer pseudocode
app.use(async (req, res, next) => {
  const ip = req.ip;
  const { remaining, reset } = await rateLimiter.check(ip);
  res.setHeader('RateLimit-Limit', '50');
  res.setHeader('RateLimit-Remaining', String(remaining));
  res.setHeader('RateLimit-Reset', String(reset)); // seconds until reset
  if (remaining <= 0) {
    res.setHeader('Retry-After', String(reset));
    return res.status(429).json({ error: 'rate_limited' });
  }
  next();
});

Consumer pattern: exponential backoff with jitter on 429 (Python tracker)

Real backoff means start at 1s, double each retry, cap at 60s, add ±25% jitter. Don't retry blindly — that's how shared cloud egress IPs get banned.

import time, random, requests

def fetch_with_backoff(url, max_retries=6):
    delay = 1.0
    for attempt in range(max_retries):
        r = requests.get(url, timeout=10)
        if r.status_code != 429:
            return r
        retry_after = r.headers.get('Retry-After')
        wait = float(retry_after) if retry_after else delay
        wait *= 1 + random.uniform(-0.25, 0.25)  # jitter
        time.sleep(min(wait, 60))
        delay = min(delay * 2, 60)
    raise RuntimeError('rate-limit retries exhausted')

Consumer pattern: client-side caching for portfolio trackers (JS)

Cache CoinGecko price responses for at least 30 seconds — prices update every 30-60s on the free tier. Stops trackers from burning rate budget on duplicate fetches.

const cache = new Map();
async function getPrice(coinId) {
  const cached = cache.get(coinId);
  if (cached && Date.now() - cached.at < 30_000) {
    return cached.value;
  }
  const r = await fetch(
    `https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=usd&include_last_updated_at=true`
  );
  if (r.status === 429) throw new Error('rate_limited');
  const value = await r.json();
  cache.set(coinId, { value, at: Date.now() });
  return value;
}

Consumer pattern: reject stale prices

Always request include_last_updated_at=true. Reject any price older than your acceptable threshold. Without this, your tracker will display stale prices indefinitely if CoinGecko's upstream feed lags.

function isFreshPrice(priceEntry, maxAgeSeconds = 60) {
  const lastUpdated = priceEntry.last_updated_at; // Unix seconds
  if (!lastUpdated) return false;
  const ageSeconds = (Date.now() / 1000) - lastUpdated;
  return ageSeconds <= maxAgeSeconds;
}

if (!isFreshPrice(price)) {
  console.warn('stale price, refusing to display');
  return null;
}

HSTS max-age below recommended (CoinGecko maintainer side)

Bump max-age to 31,536,000 (one year) at the Cloudflare edge. Required minimum for HSTS preload-list inclusion.

// Cloudflare Workers / edge config example
response.headers.set(
  'Strict-Transport-Security',
  'max-age=31536000; includeSubDomains; preload'
);

Dangerous methods advertised via OPTIONS (CoinGecko maintainer side)

Narrow Access-Control-Allow-Methods per route to the methods the route actually serves. On /ping, only GET (and HEAD) is meaningful.

// Express per-route CORS
import cors from 'cors';
app.options('/api/v3/ping', cors({ methods: ['GET', 'HEAD'] }));
app.get('/api/v3/ping', cors({ methods: ['GET', 'HEAD'] }), pingHandler);
10

Defense in Depth

For CoinGecko's maintainers, the highest-leverage change is to emit rate-limit headers on the free tier. Even an approximate signal is dramatically better than no signal — a portfolio tracker that sees RateLimit-Remaining: 3 can preemptively slow down before hitting 429, while one that sees nothing has to react to failure. Implementing the IETF draft-7 set (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset) plus a Retry-After on 429 responses would close the most-impactful finding in this audit and would measurably reduce 429-driven UI breakage in the indie tracker ecosystem.

The HSTS bump from 26 weeks to 1 year is a one-line change at the edge configuration. It costs nothing.

The advertised-methods finding is per-route configuration on whatever framework backs the API. Narrowing the Access-Control-Allow-Methods response to the methods actually accepted by each route closes the finding. On /ping specifically, only GET (and possibly HEAD) is meaningful.

The wildcard CORS and no-auth findings are both intentional and should stay as-is on the free public tier. They're the entire point of having a free public tier. The right disposition for these findings on CoinGecko's free surface is documented-and-accepted.

For tracker maintainers consuming CoinGecko, the defenses are concrete:

  • Cache aggressively. Price data on CoinGecko's free tier updates roughly every 30-60 seconds. A tracker that fetches the same price 10 times in 30 seconds is wasting budget. Cache at the client (in-memory or IndexedDB) for at least 30 seconds.
  • Implement exponential backoff with jitter on 429. Start at 1 second, double each retry, cap at 60 seconds, add ±25% jitter to avoid thundering-herd. Respect any Retry-After the server does emit, even though most CoinGecko 429s won't have one.
  • Always request include_last_updated_at=true on price endpoints. Reject any price older than your acceptable staleness threshold (60 seconds is a reasonable default for portfolio display; tighter for any settlement use, which you should not be doing against the free tier anyway).
  • Don't proxy CoinGecko through a single backend without per-user rate limiting. If you need a backend (to mask CORS or to cache server-side), enforce a per-user budget at the backend so one heavy user doesn't DoS every other user.
  • Move to the Pro tier when your tracker has real users. The free tier is for prototyping and personal projects. The moment a third party is depending on your tracker for anything that matters, the rate-limit reality of the free tier will fail them, and the migration to the Pro tier is straightforward (different host, key in x-cg-pro-api-key).
11

Conclusion

CoinGecko's free public API scored 88 with five findings — a B-grade result and the second-cleanest scan in our public-API case study series. Four of the five findings are structural to what the API actually is: a free, no-key, public market-data surface designed to be called from any client. The CRITICAL no-auth and the HIGH wildcard-CORS are correct-as-flagged by the scanner and correct-as-implemented by CoinGecko; both should stay as-is.

The one finding worth more than a passing remark is the missing rate-limit headers. CoinGecko enforces an aggressive per-IP rate limit that is famous in the indie crypto-tracker community for being the operational pain point of building on the free tier. The API has the data to emit RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers; not emitting them forces every consumer to invent their own backoff strategy, which most do badly. Adding the IETF draft-7 headers on the free tier would be the highest-impact change in this audit.

For developers building portfolio trackers, price bots, or crypto dashboards on CoinGecko's free tier, the takeaway is to cache aggressively, implement real exponential backoff with jitter on 429, request the last_updated_at field on price endpoints, and migrate to the Pro tier the moment anyone other than yourself is depending on your tracker. The free tier is for learning. Production needs a key, a budget, and rate-limit headers — and the Pro tier provides all three.

Frequently Asked Questions

Is CoinGecko safe to build a portfolio tracker on?
For prototyping and personal projects, yes. For anything with real users depending on it, the free tier's rate-limit-without-headers reality will eventually break your UI. The fix is to migrate to the Pro tier (pro-api.coingecko.com, requires an API key in x-cg-pro-api-key) the moment a third party depends on your tracker. The Pro tier emits proper rate-limit headers and gives you a budgeted, predictable contract.
What are the security differences between the free public tier and the paid Pro tier?
Three substantive differences. (1) The Pro tier requires authentication via the x-cg-pro-api-key header, so the no-auth finding here doesn't apply there. (2) The Pro tier emits rate-limit headers tied to your API-key budget, which closes the most impactful finding in this audit. (3) The Pro tier has a per-key budget rather than a per-IP budget, which means you don't get shared-egress rate-limit collisions with strangers behind the same NAT. The wildcard CORS, the HSTS max-age, and the OPTIONS-method advertisement may differ between hosts; we did not scan pro-api.coingecko.com in this audit and you should re-run a scan against that host yourself if it's the surface you depend on.
Why is 'no authentication' marked CRITICAL when CoinGecko is meant to be a public API?
Because the scanner heuristic doesn't know which APIs are intended-public and which are misconfigured. The right severity in context is informational — CoinGecko's free tier explicitly exists to be called without a key. The finding stays CRITICAL in the report so the signal is visible on APIs where no-auth would actually be a misconfiguration.
How do I handle CoinGecko's rate limits when there are no headers to tell me the budget?
Three things. (1) Cache responses aggressively client-side — prices update every 30-60 seconds, so caching for at least 30 seconds is free. (2) Implement exponential backoff with jitter on 429 (start 1s, double each retry, cap at 60s, ±25% jitter). (3) Don't retry immediately on 429 — that's how shared cloud egress IPs get banned. If you need a higher budget than the free tier provides, the answer is the Pro tier, not 'try harder against the free tier.'
Can I trust CoinGecko prices for anything financial?
For display in a personal portfolio tracker, yes. For settlement, oracle, or any use where a stale or wrong price has financial consequences, no — and you shouldn't be doing that against the free tier anyway. CoinGecko aggregates from public exchange APIs and the prices can lag the underlying exchange feeds by tens of seconds. Always request include_last_updated_at=true and reject any price older than your acceptable staleness threshold. For real financial use, source from a contractually-backed price feed (Chainlink, Pyth, or a paid CoinGecko/CoinMarketCap enterprise contract with an SLA).