Search indexing respects visibility; private excluded from results
Why it matters
A private profile that appears in site search or Google results is not private — it is merely hard to navigate to directly. Search engine indexing of private content breaches GDPR Art. 25's privacy-by-default requirement and CWE-200 (information exposure). Users who set their profiles to private expect complete exclusion from discovery, not just a locked profile page. Third-party search indices (Elasticsearch, Algolia) that ingest all records regardless of visibility are a particularly common oversight because indexing pipelines are often built before access control is finalized.
Severity rationale
Medium because the exposure is indirect — private content appears in search results rather than being directly accessible — but it defeats the user's explicit privacy intent.
Remediation
Filter visibility at the search query layer, not post-query. For Elasticsearch, apply a bool.filter clause; for Algolia, use optionalFilters or facetFilters. Emit noindex metadata for private profiles in your page generation.
Next.js generateMetadata example in src/app/u/[username]/page.tsx:
export async function generateMetadata({ params }) {
const user = await db.user.findUnique({ where: { slug: params.username } });
if (user?.profile_visibility !== 'public') {
return { robots: { index: false, follow: false } };
}
return { robots: { index: true, follow: true } };
}
For Elasticsearch, wrap searches with a visibility filter so private records are never returned regardless of requester — do not rely on post-query filtering, which can still expose counts and aggregations.
Detection
-
ID:
search-respects-privacy -
Severity:
medium -
What to look for: Enumerate every relevant item. Examine search implementation (full-text search, Elasticsearch, Algolia, or similar). Check whether the search index includes private profiles or content. Verify that search results are filtered by visibility level. Check robots.txt and meta tags to ensure private content is not indexed by web search engines.
-
Pass criteria: At least 1 of the following conditions is met. Search results (local site search and web search) exclude private profiles and content. Search indexing respects visibility tiers. Private profiles return 404 to web crawlers or have
noindextags. -
Fail criteria: Private profiles appear in search results. Elasticsearch or other indices include all content regardless of visibility. robots.txt or meta tags don't prevent indexing of private content.
-
Skip (N/A) when: The platform has no search functionality.
-
Detail on fail: Describe the search issue. Example:
"Private profiles appear in full-text search results. No visibility check in search query."or"Elasticsearch index includes all users regardless of visibility setting." -
Remediation: Filter search by visibility:
async function searchProfiles(query: string, requesterId: string) { // With Elasticsearch const results = await esClient.search({ index: 'users', body: { query: { bool: { must: [ { multi_match: { query, fields: ['name', 'bio'] } } ], filter: [ { bool: { should: [ { term: { visibility: 'public' } }, { term: { id: requesterId } }, { nested: { path: 'followers', query: { term: { 'followers.id': requesterId } } } } ] } } ] } } } }); return results.hits.hits; }Prevent web indexing of private content:
// In user profile page (Next.js example) export const generateMetadata = async ({ params }) => { const user = await db.user.findUnique({ where: { slug: params.username } }); if (user.visibility === 'private' && !isOwnProfile) { return { robots: 'noindex, nofollow' }; } return { robots: 'index, follow' }; }
External references
- cwe · CWE-285 — Improper Authorization
- owasp:2021 · A01 — Broken Access Control
- gdpr · Art. 25 — Data protection by design and by default
- cwe · CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor
Taxons
History
- 2026-04-18·v1.0.0·Initial import from community-privacy-controls·automated