Skip to content

Do not enable Elasticsearch CORS with a wildcard

Severity: mediumApplies to: Elasticsearch 8.x / 9.xApplies to: Elasticsearch 7.x
The fix/etc/elasticsearch/elasticsearch.yml
# The default and the right answer for most clusters:
http.cors.enabled: false
# If a browser app genuinely needs it, name exact origins — never '*':
# http.cors.enabled: true
# http.cors.allow-origin: "https://app.example.com"
# http.cors.allow-credentials: true
Terminal window
sudo systemctl restart elasticsearch

CORS decides which websites are allowed to make browser requests to your cluster. The dangerous configuration is the one that gets copied from old dashboard tutorials:

http.cors.enabled: true
http.cors.allow-origin: "*" # any website, anywhere

That says: any web page a user visits may make requests to this Elasticsearch cluster from that user’s browser. If the cluster is reachable from the user’s network — an internal cluster and an employee browsing the web, or any cluster and an admin on the same machine — a malicious page can query indices, and if security is off, read and delete them.

It’s a specific and underappreciated way an “internal” cluster becomes reachable: you didn’t expose it to the internet, but you let the internet’s web pages reach it through the browsers of people who can.

Old Elasticsearch browser tools — elasticsearch-head, dejavu, and various homegrown dashboards — run as static pages in the browser and talk directly to the cluster’s REST API. To work, they need CORS enabled, and the lazy configuration is allow-origin: "*".

Those tools are largely obsolete now — Kibana talks to Elasticsearch server-side, not from your browser, so it needs no CORS at all. But the setting outlives the tool. A cluster that once ran elasticsearch-head in 2019 may still have http.cors.allow-origin: "*" in its config, doing nothing useful and standing open.

Some legitimate single-page applications query Elasticsearch directly. If yours does, name the exact origins:

http.cors.enabled: true
http.cors.allow-origin: "https://app.example.com"
http.cors.allow-methods: "GET, POST"
http.cors.allow-credentials: true

Two rules:

  • Never *, and never a regex broader than you can defend. allow-origin accepts a /regex/ — a sloppy one like /https?://.*\.example\.com/ can match evil.example.com.attacker.net depending on how it’s written.
  • allow-credentials: true cannot be combined with allow-origin: "*" by the browser’s own rules — which is a hint from the web platform that wildcard plus credentials is exactly the combination that shouldn’t exist.

Better still: put the browser app behind your own backend, which talks to Elasticsearch server-side with a scoped API key. Then the cluster needs no CORS and the key never reaches the browser.