Skip to content

fix(sdk): scope endpoint base-url state per instance to harden jwks host - #118

Merged
gaokevin1 merged 1 commit into
mainfrom
shuni/task-20260629194955
Jun 30, 2026
Merged

fix(sdk): scope endpoint base-url state per instance to harden jwks host#118
gaokevin1 merged 1 commit into
mainfrom
shuni/task-20260629194955

Conversation

@omercnet

Copy link
Copy Markdown
Member

Fixes descope/etc#16631

View workflow run

Done. Fixed the JWKS-host-hijack / credential-routing hardening in descope/descope-php.

Root cause: endpoint base URLs live in static class state that DescopeSDK::__construct overwrites per instance, so a later instance could redirect an earlier one's JWKS fetch or route a management bearer to an attacker host.

Change (f17a21b, +96/−5):

  • EndpointsV1::resolveBaseUrl() — pure helper, no static mutation.
  • SDKConfig::fetchJWKSets() now builds the JWKS URL from the instance baseUrl, not the EndpointsV2 static → sibling instances can't hijack it, and the project-scoped cache (PR feat(security): NullCache fallback disables JWKS caching entirely when APCu unavailable #86) becomes effective.
  • API resolves and pins its own baseUrl; bearer secrets (mgmt key / refresh token) are refused to any host other than the configured one. Bare-projectId requests stay unrestricted.
  • Extractor validates JWT iss against the configured project ID.
  • StaticStateIsolationTest — two-instance regression (JWKS host isolation + credential-host guard).

Tests: 54/54 + 29/29 green. No new lint.

Scoped, not the full per-instance rewrite — skipped: moving every EndpointsV1/MgmtV1 auth-path static to instance state (large, risky, and the non-JWKS vectors need the same unrealistic attacker-controls-baseUrl precondition). The static auth paths are left intact since the credential-host pin in API neutralizes the actual leak regardless. Add the full migration when an SDK consumer genuinely needs concurrent multi-cluster instances in one process.


Created by Shuni 🐕

Endpoint base URLs live in static class state that DescopeSDK overwrites
for every instance in the process, so a later instance could redirect an
earlier one's JWKS fetch or route a management request (carrying
projectId:mgmtKey) to an attacker-controlled host.

Move the security-critical host resolution off the shared statics:

- Add EndpointsV1::resolveBaseUrl(), a pure helper that derives a base URL
  without mutating static state.
- SDKConfig builds the JWKS URL from the instance baseUrl instead of the
  EndpointsV2 static, so a sibling instance can no longer hijack it.
- API resolves and pins its own baseUrl; bearer credentials (management key
  or refresh token) are never sent to a host other than the configured one.
  Bare-projectId requests stay unrestricted.
- Extractor validates the JWT iss against the configured project ID.
- Add a two-instance regression test covering JWKS host isolation and the
  credential-host guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shuni-bot-dev

shuni-bot-dev Bot commented Jun 29, 2026

Copy link
Copy Markdown

🐕 Review complete — View session on Shuni Portal 🐾

@shuni-bot-dev

shuni-bot-dev Bot commented Jun 29, 2026

Copy link
Copy Markdown

🐕 Suggested Reviewers

I prioritized recent contributors who have worked across different key files involved in the change, ensuring coverage of core SDK components and testing to validate the security fix effectively.

Reviewer Reason
gaokevin1 Recently modified multiple core files including API.php, SDKConfig.php, and tests, making them well-versed in the project structure and recent changes.
dorsha Contributed to API.php and phpunit.xml, indicating familiarity with the API interface and testing framework pertinent to the security fix.

Suggested by Shuni based on git history and PR context. Names are not @-mentioned to avoid notifying anyone — request a review from whoever fits best.

@shuni-bot-dev shuni-bot-dev Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🐕 Shuni's Review

Scopes endpoint base-URL resolution per instance: JWKS fetch now uses the instance baseUrl, API pins its host and refuses bearer credentials to foreign hosts, and Extractor validates the JWT iss against the configured project.

No blocking issues found — good bones! 🦴

Traced all three new guards: the JWKS URL is byte-equivalent to the removed EndpointsV2::getPublicKeyPath().'/'.$projectId, the credential-host check is applied consistently across doGet/doPost/doDelete, and the issuer extraction mirrors the existing adjustProperties logic. The acknowledged tradeoff (multi-instance management calls fail-closed since EndpointsV1/MgmtV1 paths stay static) is the safe direction and doesn't affect single-instance usage. Woof!

@gaokevin1 gaokevin1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@gaokevin1
gaokevin1 merged commit 3d8be95 into main Jun 30, 2026
13 checks passed
@gaokevin1
gaokevin1 deleted the shuni/task-20260629194955 branch June 30, 2026 06:09
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.

2 participants