Skip to content

[Security/Medium] readPrivate dateOfBirth-search gate bypassable via Lucene query_string #1724

Closed

Description

@jlkravitz

Severity: Medium — Confidence: 0.85

Location: backend/social-work-app/lambdas/python/search/handlers/search.py:204-258 (_query_references_field / _validate_date_of_birth_permission). Identical vulnerable copy at backend/cosmetology-app/lambdas/python/search/handlers/search.py:204-258.

Not affected: compact-connect (production) — its search handler has no such gate and does not index dateOfBirth into OpenSearch, so this vector does not apply there.

Summary

The advanced search endpoint accepts a raw OpenSearch DSL query (SearchProvidersRequestSchema.query = Raw, only index/_index keys blocked) and is authorized only by @authorize_compact_level_only_action(READ_GENERAL). Searching on dateOfBirth (indexed as date, opensearch_client.py:282) is meant to require the higher readPrivate scope. The sole enforcement, _query_references_field, only flags a dict key ending in .dateOfBirth or a string value that equals/ends-with .dateOfBirth — it never inspects substrings inside a string value.

Exploit chain

A staff user with only {compact}/readGeneral (implicitly held by all staff), not readPrivate, POSTs to /v1/compacts/{compact}/providers/search:

{"query":{"bool":{"must":[
  {"term":{"providerId":""}},
  {"query_string":{"query":"licenses.dateOfBirth:[1985-01-01 TO 1985-06-30]"}}
]}}}

The value "licenses.dateOfBirth:[...]" ends with ], not .dateOfBirth, so the gate passes and the query runs. The response schema doesn't echo DOB, but hit-vs-no-hit + total is a boolean oracle — binary-searching the date range (~15 requests) yields any provider's exact DOB, defeating the readGeneral/readPrivate boundary. wildcard/regexp/script types are also unrestricted.

Reachability

Wired & real on main. Route added in stacks/search_api_stack/v1_api/provider_search.py (add_method('POST',...)), scope ['{compact}/readGeneral'] (v1_api/api.py:29-40); SearchApiStack instantiated at pipeline/backend_stage.py:197 for both apps; dateOfBirth is in the index mapping (type: date).

Suggested fix

Don't rely on field-name string matching over attacker-controlled DSL.

  • Preferred: allowlist safe query types (term/terms/match/bool/nested/range) and reject free-form query_string/simple_query_string/wildcard/regexp/script.
  • Minimal stopgap: match the dateOfBirth substring inside any string value and reject fields specs containing *.
  • Best long-term: OpenSearch field-level masking.

Apply to both social-work-app and cosmetology-app copies.

Activity

  1. added theissue type on Jul 6, 2026
  2. jlkravitz commented on Jul 6, 2026

    @jlkravitz
    CollaboratorAuthor

    @landonshumway-ia Claude found this issue. I have not verified it. Let me know what you think.

  3. landonshumway-ia commented on Jul 14, 2026

    @landonshumway-ia
    Collaborator

    @jlkravitz Has this been verified yet? If you are able to verify it, I'll pull it into the backlog.

  4. jlkravitz commented on Jul 14, 2026

    @jlkravitz
    CollaboratorAuthor

    @landonshumway-ia Just verified this (thanks for your help!). Using a token with only socw/readGeneral, a direct dateOfBirth query is correctly blocked (400 readPrivate), but the same filter smuggled through a Lucene query_string (licenses.dateOfBirth:[lo TO hi]) slips past the gate and runs.

    The script I have recovered a provider's exact DOB in 16 requests, matching the DynamoDB ground truth.

  5. landonshumway-ia commented on Jul 23, 2026

    @landonshumway-ia
    Collaborator

    Correlated with #1745

  6. moved this from Backlog to In progress in CompactConnecton Jul 29, 2026
  7. moved this from In progress to CSG Review in CompactConnecton Aug 3, 2026
  8. moved this from CSG Review to Done in CompactConnecton Aug 4, 2026
  9. closed this as completedby moving to Done in CompactConnecton Aug 4, 2026
  10. jlkravitz commented on Aug 5, 2026

    @jlkravitz
    CollaboratorAuthor

    @landonshumway-ia I ran the script (which originally reproduced this vulnerability), and I have confirmed that it no longer runs successfully now that #1805 has been merged and deployed into the test environment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions