Search endpoints are frequently overlooked during tenant-isolation reviews because they feel like read-only features. But a full-text search that runs across a shared index returns matching documents from every tenant that contains the search term — exposing business names, customer data, proprietary content, and internal notes to anyone who knows what to search for. CWE-285 and OWASP A01 apply; Algolia and Elasticsearch indices that lack per-tenant filters are particularly dangerous because the search engine optimizes for broad recall.
High because an unfiltered search index is a structured reconnaissance tool that any authenticated user can use to extract data from arbitrary tenants.
For Algolia, generate tenant-scoped secured API keys server-side so the filter is baked into the key and cannot be removed by the client:
const securedKey = algoliasearch.generateSecuredApiKey(
process.env.ALGOLIA_SEARCH_KEY,
{ filters: `organizationId:${session.user.organizationId}`, validUntil: Math.floor(Date.now() / 1000) + 3600 }
)
For Typesense, use scoped API keys with a filter condition that locks the key to the tenant's records. For database full-text search (ILIKE, tsvector), always AND organizationId = $1 alongside the search predicate in src/app/api/search/route.ts. Autocomplete endpoints need the same treatment — they are search endpoints too.
ID: saas-multi-tenancy.tenant-boundaries.search-filter-no-cross-tenant
Severity: high
What to look for: Examine all search endpoints, filter endpoints, and autocomplete endpoints. Look for search integrations (Algolia, Typesense, Elasticsearch, Meilisearch, database full-text search). Check whether search indexes are shared across tenants and whether queries are filtered to the current tenant before results are returned. Also check filter endpoints that accept arbitrary field values from the client — these can sometimes be exploited to filter across tenant boundaries.
Pass criteria: List all search query endpoints and confirm at least 100% include a tenant filter either: (a) at the search index level (Algolia filters, Typesense scoped API keys, Elasticsearch query-time filters), or (b) at the database level before results are returned. Autocomplete endpoints are similarly scoped.
Fail criteria: Search results include records from other tenants. A full-text search for a term returns matching documents from any tenant that has that term in their data. Filter parameters from the client can be used to traverse tenant boundaries.
Skip (N/A) when: No search functionality or full-text search integration is detected. Signal: no search-related API endpoints (/api/search, /api/autocomplete), no Algolia/Typesense/Elasticsearch dependencies, no database LIKE, ILIKE, or full-text search operator patterns.
Detail on fail: Describe the search system and missing tenant filter. Example: "Algolia index 'documents' contains records from all tenants. Search queries in src/app/api/search/route.ts do not apply an Algolia filter for organizationId. Results from any tenant with matching content are returned."
Remediation: For Algolia, use secured API keys with a tenant filter baked in:
// Generate a tenant-scoped secured API key server-side
const securedKey = algoliasearch.generateSecuredApiKey(
process.env.ALGOLIA_SEARCH_KEY,
{ filters: `organizationId:${session.user.organizationId}`, validUntil: ... }
)
For database full-text search, always add the tenant condition to the WHERE clause alongside the search predicate. For Typesense, use scoped API keys or include the tenant filter in every search request.