Skip to content

Fix SSRF in Legacy nextPage() — validate URL before sending authenticated request#403

Open
OleksiiKaliuzhnyi wants to merge 4 commits into
masterfrom
appsec-fix-514600
Open

Fix SSRF in Legacy nextPage() — validate URL before sending authenticated request#403
OleksiiKaliuzhnyi wants to merge 4 commits into
masterfrom
appsec-fix-514600

Conversation

@OleksiiKaliuzhnyi
Copy link
Copy Markdown

Why?

nextPage() in the Legacy client passed the $pages->next URL directly to sendRequest() without validating it. Because authenticateRequest() attaches Bearer/Basic credentials to every outbound request unconditionally, a caller with attacker-controlled pagination data could cause the SDK to make an authenticated GET to an arbitrary host — leaking credentials or reaching internal metadata endpoints.

How?

Added a parse_url() guard in nextPage() that throws \InvalidArgumentException if the URL scheme is not https or the host is not api.intercom.io. This is consistent with every other dispatch path in the SDK, which all hardcode https://api.intercom.io/. Five PHPUnit test cases cover valid URLs, wrong scheme, wrong host, mixed-case variants, and a null next-page value.

Generated with Claude Code

…om/intercom#514600)

nextPage() passed $pages->next directly to sendRequest() with no host
validation. authenticateRequest() then unconditionally attached Bearer/Basic
credentials to every outbound request, enabling credential exfiltration and
SSRF against arbitrary hosts including the AWS instance metadata endpoint.

Every other dispatch method in this class hardcodes https://api.intercom.io/
as the base; nextPage() was the sole exception. The fix applies the same
constraint via parse_url() before the request is sent.
Five PHPUnit cases: happy-path (https://api.intercom.io passes with
credentials attached), attacker host rejected, AWS metadata URL rejected,
plain http:// rejected even for correct host, subdomain bypass rejected.
@OleksiiKaliuzhnyi OleksiiKaliuzhnyi self-assigned this May 26, 2026
Logs confirm Intercom-PHP/4.4.0 is actively used against api.eu.intercom.io
(17k requests/week, 1 EU app). The original hardcoded guard would have broken
nextPage() for regional customers because the Intercom API returns next-page
URLs on the same host the request arrived at.

Changes:
- Add private $baseUrl property (default: https://api.intercom.io)
- Add setBaseUrl() setter to target EU/AU regional endpoints
- Update post/put/delete/get to use $this->baseUrl (no behaviour change for
  existing callers — default is unchanged)
- nextPage() now validates the next-page URL host against parse_url($this->baseUrl),
  keeping the SSRF guard while allowing regional pagination

5 new test cases cover EU/AU happy paths, US↔EU mismatch rejection, and
SSRF rejection even when a regional baseUrl is set.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
normprovost
normprovost previously approved these changes May 26, 2026
Comment thread src/Legacy/IntercomClient.php Outdated
The configurable-baseUrl approach required explicit user action to work
for EU/AU customers — no existing caller sets a base URL, so it would
not have fixed the regional pagination case in practice.

Simpler approach: validate that the next-page URL is https:// and the
host ends with .intercom.io. This covers api.intercom.io (US),
api.eu.intercom.io (EU), api.au.intercom.io (AU), and any future
regional endpoint without configuration changes.

The .intercom.io suffix check (with the leading dot) prevents the
bypass string evilintercom.io from matching.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
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