fix: match the demo endpoint on a segment boundary, not a raw prefix (#23) - #30
Merged
Merged
Conversation
…23) `str_starts_with($path, '/v1/demo')` also matched paths that merely start with those characters but are answered by an authenticated endpoint: /v1/demographics, /v1/demo-prices, and /v1/demo/../prices/latest (which the server resolves to /v1/prices/latest). Those requests went out with no Authorization header, came back 401, and the SDK reported "Invalid API key." to callers whose key was perfectly valid. Demo detection is now a segment-boundary match: exactly /v1/demo, or a path below /v1/demo/. Any path containing a . or .. segment - percent-encoded forms included - is never treated as demo, because the characters we inspect are not the endpoint that answers. Keyless demo mode is unchanged: demoPrices() still sends no Authorization header and still works with no key, and the "No API key configured" guidance still fires for every non-demo path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #23
The defect, re-verified on current main (0672df0)
src/Client.php:293decided keyless demo mode with a raw prefix match:That is true for any path whose characters start that way, not just the demo
endpoint. Those requests ship with no
Authorizationheader, the APIanswers 401, and the SDK reports
Invalid API key.to a customer whose key isperfectly valid.
/v1/demo/prices/v1/demo/v1/demographics/v1/demo-prices/v1/demo/../prices/latest/v1/prices/latest/v1/demo/%2e%2e/prices/latest/v1/prices/latestStatus: confirmed live on main today. Nothing in the 3.0.0 release moved
this code; the prefix match is still verbatim at line 293.
The contract chosen: segment boundary, and dot segments are never demo
Per the issue, stating the choice explicitly.
Chosen: segment-boundary match —
$bare === '/v1/demo'orstr_starts_with($bare, '/v1/demo/'), where$bareis the path with anyquery string or fragment stripped.
Why not an exact match on
/v1/demo/prices: the server owns the demonamespace. If a
/v1/demo/latestships tomorrow, an exact match makes the SDKsend a key to an endpoint that does not want one and silently regress keyless
mode for anyone on an older SDK. Segment boundary tracks the namespace.
One addition the boundary alone does not cover.
/v1/demo/../prices/latestdoes sit under
/v1/demo/by character, but the server resolves the dotsegments and answers from
/v1/prices/latest, which needs the key. So any pathwith a
.or..segment is never treated as demo. Percent-encoded forms(
%2e,%2E) are decoded before the check, because some servers decode beforeresolving.
The failure directions are not symmetric, and the rule errs toward the cheap
one: treating a demo path as authenticated sends a valid key to our own origin
(already fenced by
assertSameOrigin), while treating an authenticated path asdemo withholds the key and produces a wrong error message at the customer.
Keyless demo behaviour is preserved:
demoPrices()still works with no key andstill sends no
Authorizationheader even when a key is configured, and the"No API key configured … keyless demo mode is available via
$client->demoPrices()" exception still fires for every non-demo path.TDD evidence
New file
tests/DemoPathDetectionTest.php— 14 tests. Two data-driven casesover five non-demo paths (the key must be sent; a keyless client must still get
the helpful exception and send nothing), plus four regression guards on real
demo paths.
RED — test file against unmodified
src/Client.phpfromorigin/mainProduced by
git checkout origin/main -- src/Client.phpafter the fix waswritten, then
./vendor/bin/phpunit tests/DemoPathDetectionTest.php:The red is exactly the defect: the
Authorizationheader is absent from theoutbound request the fake transport recorded for
/v1/demographics, and akeyless client silently sent a request instead of raising.
GREEN — same test file, with the fix
Full suite
origin/mainOK (229 tests, 747 assertions)OK (243 tests, 773 assertions)No behaviour change outside
Client::request()'s demo decision; no versionbump, no CHANGELOG entry (
VersioningTestpins the top CHANGELOG heading toClient::VERSION, so a release note belongs with the next release commit).🤖 Generated with Claude Code
https://claude.ai/code/session_015ao5paex73xXvuM424Libo