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.
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 atbackend/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 indexdateOfBirthinto OpenSearch, so this vector does not apply there.Summary
The advanced search endpoint accepts a raw OpenSearch DSL query (
SearchProvidersRequestSchema.query = Raw, onlyindex/_indexkeys blocked) and is authorized only by@authorize_compact_level_only_action(READ_GENERAL). Searching ondateOfBirth(indexed asdate,opensearch_client.py:282) is meant to require the higherreadPrivatescope. The sole enforcement,_query_references_field, only flags a dict key ending in.dateOfBirthor 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), notreadPrivate, 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 thereadGeneral/readPrivateboundary.wildcard/regexp/scripttypes are also unrestricted.Reachability
Wired & real on
main. Route added instacks/search_api_stack/v1_api/provider_search.py(add_method('POST',...)), scope['{compact}/readGeneral'](v1_api/api.py:29-40);SearchApiStackinstantiated atpipeline/backend_stage.py:197for both apps;dateOfBirthis in the index mapping (type: date).Suggested fix
Don't rely on field-name string matching over attacker-controlled DSL.
term/terms/match/bool/nested/range) and reject free-formquery_string/simple_query_string/wildcard/regexp/script.dateOfBirthsubstring inside any string value and rejectfieldsspecs containing*.Apply to both
social-work-appandcosmetology-appcopies.