Skip to content

search-service: stop trusting X-User-ID, gate index endpoints behind service token, keep auth on in deploys - #1561

Open
devin-ai-integration[bot] wants to merge 7 commits into
mainfrom
devin/1789396851-search-service-auth-bypass
Open

devin-ai-integration[bot] wants to merge 7 commits into
mainfrom
devin/1789396851-search-service-auth-bypass

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Vulnerability: authentication bypass / broken access control in the search-service auth middleware (CWE-287 Improper Authentication, CWE-290 Authentication Bypass by Spoofing, CWE-208 Observable Timing Discrepancy). Severity: medium. Trust boundary: anything that can reach the search-service pod directly (other pods in the namespace, anyone bypassing the API gateway) — the gateway is the only component that validates JWTs, and the service assumed every caller had come through it.

Before: app/middleware/auth.py accepted any request that carried a nonempty X-User-ID header, so a direct caller could read any user's search results by setting that header. REQUIRE_AUTH=false (set by scripts/deploy-dev.sh and scripts/lib/tenant-common.sh) turned the middleware off entirely, so on deployed tenants nothing was checked at all and omitting the header gave an unscoped search. Index/reindex routes shared the same rule as user routes, so anyone with a user identity could write to or wipe the index. The service token was compared with ==.

After: the middleware validates identity itself, has no off switch, and separates the two endpoint classes:

# before_request
g.user_id = None
if public path (/health, /metrics): allow
token = bearer()
if token and hmac.compare_digest(token, SEARCH_SERVICE_TOKEN): allow          # service callers
payload = jwt.decode(token, JWT_SECRET, ["HS256","HS384","HS512"]) or -> 401
if payload.type == "refresh": -> 401                                           # auth-service refresh tokens are not access credentials
user_id = payload.sub|user_id or -> 401
if request.blueprint == "index": -> 403                                        # user JWT never reaches index/reindex/delete
g.user_id = user_id
  • AuthConfig.require_auth / REQUIRE_AUTH is removed. Protected routes fail closed; a missing JWT_SECRET or SEARCH_SERVICE_TOKEN is logged at startup and rejects every request on the corresponding endpoint class.
  • search.py handlers scope by g.user_id instead of request.headers["X-User-ID"], so the header is ignored even when present.
  • HS512 is accepted because auth-service's JwtTokenProvider uses JJWT signWith(key) on a 64-byte HMAC key, which selects HS512.
  • PyJWT==2.13.0 added to requirements.txt (2.10.1 has two high-severity CVEs flagged by dependency scan).
  • Deploy config: deploy-dev.sh and deploy-tenant.sh generate SEARCH_SERVICE_TOKEN (or take a stable one from the env, like JWT_SECRET) and inject it plus JWT_SECRET into search-service; docker-compose.yml gets local-dev placeholders. The REQUIRE_AUTH=true setting was dropped since the service no longer reads it.
  • The search-service pod template gets a checksum/secrets annotation so a regenerated SEARCH_SERVICE_TOKEN/JWT_SECRET rolls the pods instead of leaving them on the old value.
  • Known follow-up (pre-existing, not changed here): on Kubernetes nothing populates the search index through a supported path. SQS_ENABLED=false was already set on main, and the SNS->SQS search subscription filters on an eventType attribute that neither document-service (event_type) nor file-service (no attributes) sets. The only thing that could write to the index in a deploy was a user JWT via the gateway, which is the bypass this PR closes. Wiring the eventing pipeline is a separate change.
  • shared/openapi/search-service.yaml: X-User-ID marked deprecated/ignored; identity comes from the bearer JWT.
  • .gitleaksignore pins two commit-scoped fingerprints for the earlier literal test JWT secret in this branch's history (now a derived value in tests/conftest.py); the rule still fires on any new occurrence.

Tests: services/search-service/tests/test_auth.py (new, 21 cases) runs the app with real credentials configured and covers the attack path: forged X-User-ID → 401, REQUIRE_AUTH=false in the env has no effect → 401, wrong-secret/alg=none JWT → 401, refresh token → 401, valid HS256/HS384/HS512 JWT accepted and results scoped to the JWT subject even when X-User-ID says otherwise, user JWT on index/reindex/delete → 403, wrong service token → 401, correct one → allowed. tests/api/test_search_flow.py (black-box, run against a gateway) previously indexed documents with a user JWT; it now asserts that returns 403 and keeps the search/suggest/advanced and 400-validation checks.

Search-service suite: 62 passed. ruff check clean on changed service files (remaining BLE001/I001 findings are pre-existing in untouched code). gitleaks detect --log-opts origin/main..HEAD clean locally.

Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/80d79842d8934d5c82c6eaa504128ab1
Open in Devin Desktop: https://partner-workshops.devinenterprise.com/desktop/session/80d79842d8934d5c82c6eaa504128ab1?variant=devin
Requested by: @mbatchelor81


Devin Review

…ken, keep auth on in deploys

Fixes auth bypass (CWE-287/CWE-290/CWE-208): the middleware trusted any
X-User-ID header, was disabled via REQUIRE_AUTH=false in deploy scripts,
did not separate internal index/reindex routes from user routes, and
compared the service token with ==.
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

…esh tokens, provision SEARCH_SERVICE_TOKEN

- Drop AuthConfig.require_auth: there is no longer any switch that makes the
  middleware trust X-User-ID or skip credentials. Unit tests authenticate with
  the test service token instead of turning auth off.
- Accept HS512 (auth-service signs with JJWT signWith(key) on a 64-byte secret,
  which selects HS512) and reject tokens carrying type=refresh.
- deploy-dev.sh / deploy-tenant.sh generate SEARCH_SERVICE_TOKEN (or take a
  stable one from the env) and inject it into search-service; docker-compose
  gets a local-dev placeholder.
- Test secrets are derived, not literals; .gitleaksignore pins the two
  historical fingerprints of the earlier test-only literal.
devin-ai-integration[bot]

This comment was marked as resolved.

…pods on secret change

With user JWTs no longer able to write to the index, the SNS->SQS subscription
Terraform already provisions for search-service is the indexing path for
Kubernetes deploys. deploy-dev.sh now reads sqs_search_indexing_queue_url and
enables the consumer when it is available. Tenants keep SQS_ENABLED=false
(shared topic, no per-tenant queue).

The search-service pod template carries a checksum of its Secret so a
regenerated SEARCH_SERVICE_TOKEN or JWT_SECRET forces a rollout instead of
leaving a running pod on the previous value.
devin-ai-integration[bot]

This comment was marked as resolved.

The SNS->SQS search subscription filters on an eventType message attribute
that no publisher sets (document-service sends event_type, file-service sets
none), so turning the consumer on delivers nothing. Wiring the indexing
pipeline is a separate change; this PR keeps the pre-existing SQS_ENABLED=false
and only removes the user-JWT write path to the index.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment thread scripts/deploy-dev.sh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant