Skip to content

fix(sdk): keep API response endpoints on the configured baseApiUrl - #1165

Merged
ted-uneven merged 2 commits into
mainfrom
ted/int2-1688-vulnerability-report-sdk-executes-unbound-api-steps
Nov 10, 2026
Merged

ted-uneven merged 2 commits into
mainfrom
ted/int2-1688-vulnerability-report-sdk-executes-unbound-api-steps

Conversation

@ted-uneven

Copy link
Copy Markdown
Collaborator

No description provided.

@vercel

vercel Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
relay-sdk-demo Ready Ready Preview Nov 10, 2026 3:57pm UTC

Request Review

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @ted-uneven's task in 1m 45s —— View job


📝 Docs Drift Check — Confidence: MEDIUM

What changed: The SDK now requires baseApiUrl to point at an origin you control and enforces that step endpoints returned in API responses resolve to that same origin — any endpoint resolving to a different host fails the execution, so the API key is only sent to the configured origin.

Docs impact: The createClient reference documents baseApiUrl but does not state the new origin constraint. The option's source doc comment was updated with a security instruction ("Only set this to a proxy you control"), which the reference page should mirror.

Affected pages (paths inside relayprotocol/relay-docs):

  • references/relay-kit/sdk/createClient.mdx — update

Draft:

> Replace section: `## Options`

## Options

| Property                            | Description                                                                                                                                     | Required |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| **baseApiUrl**                      | The base API url to use when making requests. You can use the `MAINNET_RELAY_API` or the `TESTNET_RELAY_API` constants exported by the package, or pass in your own proxy API url to protect your API key on the client. Only set this to the Relay API or a proxy you control. | ✅      |
| **apiKey**                          | A Relay API key to use when making requests. Only pass in this property when the Relay client instance is used exclusively server-side. Otherwise you'll leak your key to the client.                                                                                                  | ❌       |
| **chains**                          | A list of Relay Chains, by default a list of backup chains is configured                                                                       | ❌       |
| **source**                          | The source to associate your onchain activity with, should map to a domain                                                                     | ❌       |
| **logLevel**                        | The level at which to log, refer to the `LogLevel` enum exported by the package                                                                 | ❌       |
| **pollingInterval**                 | How often to poll status and check apis, this value is in milliseconds                                                                          | ❌       |
| **maxPollingAttemptsBeforeTimeout** | The source to associate your onchain activity with, should be set to a domain                                                                     | ❌       |

<Warning>
  Only set **`baseApiUrl`** to the Relay API or a proxy you control. The SDK resolves the step endpoints returned in API responses against this origin and rejects any that resolve to a different host, so your API key is only ever sent to the configured origin.
</Warning>

Needs human input:

  • Confirm whether this constraint also warrants a one-line note on references/relay-kit/sdk/installation.mdx (where a proxy baseApiUrl is first introduced). I left it out to avoid duplicating the security callout — the createClient page is the canonical home for the option.

To publish as a draft PR in relayprotocol/relay-docs, reply on this PR with:

/publish-docs-pr

A separate workflow will open a draft PR attributed to you. Edit the draft freely before marking it ready for review.

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @ted-uneven's task in 1m 31s —— View job


Code Review — No issues found.

Reviewed 7 files, 55/-11 lines changed. Checked correctness, security, error handling, and repo guidelines compliance.

This is a clean, well-targeted fix for the SSRF / API-key-leak vector (SDK executing API-response step endpoints that resolve off the configured baseApiUrl). A few things I verified:

  • Origin comparison is the right check. resolveApiUrl (packages/sdk/src/utils/apiKey.ts:27) compares new URL(...).origin against the base origin, which covers scheme, host, and port. The userinfo trick (@evil.com/...), host-suffix tricks (.evil.com), and port redirection (:8080) all produce a different origin and correctly throw. isRelayApiUrl was tightened the same way (prefix match → origin match), so the API key only goes to a true Relay origin.
  • Complete coverage. Every API-provided-endpoint concatenation now routes through resolveApiUrl: the signature post endpoint (signatureStep.ts:93), both status-check endpoints (signatureStep.ts:297, transaction.ts:316). The remaining ${request.baseURL}/... concatenations at transaction.ts:590 and :650 are hardcoded literal paths (/transactions/single, /transactions/index), not attacker-controlled, so they don't need guarding.
  • Fails closed before any request. resolveApiUrl is called at signatureStep.ts:93, before the header construction and the try/axios.request block, so the throw propagates up and no request fires. The added test (signatureStep.test.ts:233) asserts exactly this — axios.request is never called for the @evil.com/execute/permits endpoint.
  • isRelayApiUrl now tolerates malformed input via the try/catch returning false, which is a safe default for a header-gating function.

No regression for proxy setups: a path-prefixed baseApiUrl (e.g. https://proxy.example/relay) keeps its origin when the endpoint is appended, so legitimate relative endpoints still resolve on-origin. The changeset also correctly documents the intentional behavior change (off-host endpoints now fail).

@greptile-apps

greptile-apps Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issue was found.

Summary

This PR keeps response-provided endpoints on the configured baseApiUrl origin.

  • Signature posts and status checks stay on the configured API host.
  • API keys only go to URLs with a known Relay API origin.

The earlier relative-proxy and swallowed-error findings are addressed. ted-uneven manually resolved the API-key-test thread without explanation. All supplied previous threads were unnumbered, so no numbered previous-finding entries are required.

Reviews (3) · Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." · Reviewed by Greptile

Comment thread packages/sdk/src/utils/apiKey.ts Resolved
Comment thread packages/sdk/src/utils/transaction.ts Resolved
Comment thread packages/sdk/src/utils/apiKey.ts Resolved
@ted-uneven
ted-uneven force-pushed the ted/int2-1688-vulnerability-report-sdk-executes-unbound-api-steps branch from e977d3a to f88e257 Compare October 29, 2026 15:41
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @ted-uneven's task in 1m 26s —— View job


Code Review — No issues found.

Reviewed 8 files, 83/-21 lines changed. Checked correctness, security, error handling, and repo guidelines compliance.

This is a clean fix for the SSRF / API-key-leak vector. I focused on whether the three findings from the earlier Greptile pass are resolved in the current commit (f88e2577) — all three are:

  • Relative proxies keep status checks (was P1). resolveApiUrl (packages/sdk/src/utils/apiKey.ts:27) now resolves against the page origin — new URL(\${baseUrl}${endpoint}`, pageOrigin)withpageOrigin = window.location?.origin. So baseApiUrl: '/api/relay'resolves tohttps:///api/relay/status, whose origin matches new URL('/api/relay', pageOrigin).origin`, and the check passes instead of throwing.
  • Rejected endpoints fail fast instead of timing out (was P2). resolveApiUrl is invoked outside the swallow-and-retry path in both loops: signatureStep.ts:289 is before the while (attemptCount < maximumAttempts) loop, and transaction.ts:314 sits inside the while but outside the try/catch at :315. A thrown "not on origin" error therefore propagates up on the first pass rather than being replaced with an empty 200 response and retried to a timeout.
  • API-key rejection is now tested (was P2). actions/apiKey.test.ts:70 asserts that https://api.relay.link.evil.com and https://api.relay.link@evil.com do not receive x-api-key, locking in the origin-match behavior of isRelayApiUrl.

Security of the origin check holds up: the userinfo (@evil.com), host-suffix (.evil.com), and port tricks all produce a different URL.origin and throw. isRelayApiUrl is tightened to origin comparison with a try/catch returning false on malformed input, which is a safe default for a header-gating function. The hardcoded-literal concatenations elsewhere in transaction.ts (/transactions/single, /transactions/index) aren't attacker-controlled and correctly stay unguarded.

Two minor non-blocking notes (not worth changing):

  • resolveApiUrl runs once per poll iteration in transaction.ts (recomputing the same URL); negligible cost since it's deterministic and throws on the first pass if invalid.
  • The changeset accurately documents the intentional behavior change (off-origin endpoints now fail executions).

The changeset also follows the repo's changelog guidance — it leads with the reader-facing outcome.

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @ted-uneven's task in 1m 17s —— View job


📝 Docs Drift Check — Confidence: MEDIUM

What changed: The SDK now treats the step endpoints returned in API responses as resolving against the configured baseApiUrl origin, and rejects any that resolve to a different host — so the API key is only ever sent to the configured origin. The baseApiUrl option doc comment gained a security instruction ("Only set this to a proxy you control").

Docs impact: The createClient reference documents baseApiUrl but doesn't state the new origin constraint, nor warn that it must point at an origin you control. The option's canonical reference page should mirror the new guidance.

Affected pages (paths inside relayprotocol/relay-docs):

  • references/relay-kit/sdk/createClient.mdx — update

Draft:

> Replace section: `## Options`

## Options

| Property                            | Description                                                                                                                                     | Required |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| **baseApiUrl**                      | The base API url to use when making requests. You can use the `MAINNET_RELAY_API` or the `TESTNET_RELAY_API` constants exported by the package, or pass in your own proxy API url to protect your API key on the client. Only set this to the Relay API or a proxy you control. | ✅      |
| **apiKey**                          | A Relay API key to use when making requests. Only pass in this property when the Relay client instance is used exclusively server-side. Otherwise you'll leak your key to the client.                                                                                                  | ❌       |
| **chains**                          | A list of Relay Chains, by default a list of backup chains is configured                                                                       | ❌       |
| **source**                          | The source to associate your onchain activity with, should map to a domain                                                                     | ❌       |
| **logLevel**                        | The level at which to log, refer to the `LogLevel` enum exported by the package                                                                 | ❌       |
| **pollingInterval**                 | How often to poll status and check apis, this value is in milliseconds                                                                          | ❌       |
| **maxPollingAttemptsBeforeTimeout** | The source to associate your onchain activity with, should be set to a domain                                                                     | ❌       |

<Warning>
Only set **`baseApiUrl`** to the Relay API or a proxy you control. The SDK resolves the endpoints returned in API responses against this origin and rejects any that resolve to a different host, so your API key is only ever sent to the configured origin.
</Warning>

Needs human input:

  • The <Warning> sits directly under the options table per the SDK-reference profile (§3.4) — confirm placement reads well alongside the existing singleton warning on installation.mdx.
  • I left references/relay-kit/sdk/installation.mdx (where a proxy baseApiUrl is first introduced) untouched to avoid duplicating the security callout; createClient is the canonical home for the option. Confirm whether a one-line pointer there is wanted.

To publish as a draft PR in relayprotocol/relay-docs, reply on this PR with:

/publish-docs-pr

A separate workflow will open a draft PR attributed to you. Edit the draft freely before marking it ready for review.

@ted-uneven
ted-uneven force-pushed the ted/int2-1688-vulnerability-report-sdk-executes-unbound-api-steps branch from f88e257 to 2806ba8 Compare October 29, 2026 15:43
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @ted-uneven's task in 1m 48s —— View job


Code Review — No issues found.

Reviewed 7 files, 55/-11 lines changed. Checked correctness, security, error handling, and repo guidelines compliance.

This is a clean, well-targeted fix for the SSRF / API-key-leak vector (SDK executing API-response step endpoints that resolve off the configured baseApiUrl). What I verified:

  • Origin check is sound and complete. resolveApiUrl (packages/sdk/src/utils/apiKey.ts:27) builds new URL(\${baseUrl}${endpoint}`, pageOrigin)and compares.originagainstnew URL(baseUrl, pageOrigin).origin — covering scheme, host *and* port. The userinfo trick (@evil.com/…→ originevil.com), host-suffix trick (.evil.com), and port redirection (:8080) all produce a different origin and throw. isRelayApiUrl` was tightened the same way (prefix → origin), so the key is only ever sent to a true Relay origin.
  • Every attacker-controlled concatenation is guarded. The signature POST endpoint (signatureStep.ts:93) and both status-check endpoints (signatureStep.ts:289, transaction.ts:314) now route through resolveApiUrl. The remaining ${request.baseURL}/… concatenations (transaction.ts:591, :651) are hardcoded literal paths (/transactions/single, /transactions/index), not from API responses, so they correctly stay unguarded.
  • Relative proxies still work (prior Greptile P1 resolved). baseApiUrl: '/api/relay' resolves against window.location.origin, so the endpoint and the base resolve to the same app origin and the check passes rather than throwing after the wallet has signed.
  • Fails fast, not into a timeout (prior Greptile P2 resolved). resolveApiUrl is invoked before the axios.request try/catch in both loops — signatureStep.ts:289 is above the while, and transaction.ts:314 sits outside the swallow-and-retry try at :315. A rejected endpoint propagates immediately instead of being replaced by an empty 200 and polled to a timeout.
  • Regression test present (prior Greptile P2 resolved). signatureStep.test.ts:233 asserts axios.request is never called for an off-origin POST endpoint, and isRelayApiUrl's try/catch returns false on malformed input — a safe default for a header-gating function.

No regression for path-prefixed proxies (https://proxy.example/relay) since origin is preserved when the endpoint is appended, and the v2→v3 status-endpoint override still runs before resolution. The changeset leads with the reader-facing outcome and documents the intentional behavior change (off-origin endpoints now fail executions), and the baseApiUrl doc comment gained the matching security note.

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @ted-uneven's task in 49s —— View job


📝 Docs Drift Check — Confidence: MEDIUM

What changed: The SDK now resolves the step endpoints returned in API responses against the configured baseApiUrl origin and rejects any that resolve to a different host, so the API key is only ever sent to the configured origin. The baseApiUrl option doc comment gained a security instruction ("Only set this to a proxy you control").

Docs impact: The createClient reference documents baseApiUrl but doesn't state the new origin constraint, nor warn that it must point at an origin you control. The option's canonical reference page should mirror the new guidance.

Affected pages (paths inside relayprotocol/relay-docs):

  • references/relay-kit/sdk/createClient.mdx — update

Draft:

> Replace section: `## Options`

## Options

| Property                            | Description                                                                                                                                     | Required |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| **baseApiUrl**                      | The base API url to use when making requests. You can use the `MAINNET_RELAY_API` or the `TESTNET_RELAY_API` constants exported by the package, or pass in your own proxy API url to protect your API key on the client. Only set this to the Relay API or a proxy you control. | ✅      |
| **apiKey**                          | A Relay API key to use when making requests. Only pass in this property when the Relay client instance is used exclusively server-side. Otherwise you'll leak your key to the client.                                                                                                  | ❌       |
| **chains**                          | A list of Relay Chains, by default a list of backup chains is configured                                                                       | ❌       |
| **source**                          | The source to associate your onchain activity with, should map to a domain                                                                     | ❌       |
| **logLevel**                        | The level at which to log, refer to the `LogLevel` enum exported by the package                                                                 | ❌       |
| **pollingInterval**                 | How often to poll status and check apis, this value is in milliseconds                                                                          | ❌       |
| **maxPollingAttemptsBeforeTimeout** | The source to associate your onchain activity with, should be set to a domain                                                                     | ❌       |

<Warning>
Only set **`baseApiUrl`** to the Relay API or a proxy you control. The SDK resolves the endpoints returned in API responses against this origin and rejects any that resolve to a different host, so your API key is only ever sent to the configured origin.
</Warning>

Needs human input:

  • The <Warning> sits directly under the options table per the SDK-reference profile (§3.4). Confirm placement reads well.
  • I left references/relay-kit/sdk/installation.mdx (where a proxy baseApiUrl is first introduced) untouched to avoid duplicating the security callout; createClient is the canonical home for the option. Confirm whether a one-line pointer there is wanted.

To publish as a draft PR in relayprotocol/relay-docs, reply on this PR with:

/publish-docs-pr

A separate workflow will open a draft PR attributed to you. Edit the draft freely before marking it ready for review.

@ted-uneven

Copy link
Copy Markdown
Collaborator Author

/publish-docs-pr

@github-actions

Copy link
Copy Markdown
Contributor

🚚 Docs Draft PR — starting publish to relayprotocol/relay-docs, requested by @ted-uneven. I'll post the PR link here when it's ready.

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @ted-uneven's task in 55s —— View job


Docs update materialized ✅

Applied the drift-check draft to the cloned relayprotocol/relay-docs working tree. A separate workflow will open the draft PR attributed to @ted-uneven.

  • Load the drift-check draft
  • Read docs style guide + target file
  • Materialize the createClient.mdx update
  • Verify style-guide conformance
  • Emit publish summary

Files changed

  • references/relay-kit/sdk/createClient.mdx — update
    • Appended "Only set this to the Relay API or a proxy you control." to the baseApiUrl row.
    • Inserted a <Warning> directly under the options table (before the code example) describing the origin constraint on API-response endpoints.

Conformance checks

  • Frontmatter title/description present; no H1 in body.
  • Heading levels unchanged (## Options only); no skips.
  • <Warning> is 3 rendered lines (well under the 15-line cap); no nested code/tables.
  • Existing typescript example preserved — Replace section applied surgically, not a full rewrite.

Notes for the reviewer

  • The drift draft flagged two items for human input, carried forward here:
    • <Warning> placement sits directly under the table per the SDK-reference profile (§3.4) — confirm it reads well alongside any existing callouts.
    • references/relay-kit/sdk/installation.mdx (where a proxy baseApiUrl is first introduced) was left untouched to avoid duplicating the security callout; createClient is the canonical home. Decide whether a one-line pointer there is wanted.
DOCS_PUBLISH_SUMMARY_START
branch: <nothing here — the runner already set it>
files_changed:
  - path: references/relay-kit/sdk/createClient.mdx
    action: update
notes:
  - Applied "Replace section: ## Options" surgically via two Edits (row amendment + Warning insert) to preserve the existing typescript example, which the draft block omitted.
  - Carried forward two human-input flags from the draft: Warning placement confirmation, and whether installation.mdx needs a one-line pointer (left untouched).
DOCS_PUBLISH_SUMMARY_END

@github-actions

Copy link
Copy Markdown
Contributor

✅ Docs Draft PR opened: relayprotocol/relay-docs#536

@ted-uneven is assigned on the draft PR. Review and mark ready when it's good.

@pedromcunha-uneven
pedromcunha-uneven self-requested a review November 6, 2026 14:20
…bility-report-sdk-executes-unbound-api-steps
@github-actions

github-actions Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @ted-uneven's task in 40s —— View job


Code Review -- No issues found.

Reviewed 7 files, 67/-21 lines changed. Checked for correctness, security, error handling, and repo guidelines compliance.

What I checked:

  • Every URL built from an API response now goes through resolveApiUrl. That covers the signature POST (signatureStep.ts:99) and both status checks (signatureStep.ts:295, transaction.ts:314). The other concatenations (transaction.ts:591, :651) use fixed paths written in the SDK, so they don't need the check.
  • Endpoints on another host are rejected. Comparing URL.origin catches endpoints that start with @, end the hostname early with .evil.com, or add a :port. isRelayApiUrl now uses the same origin comparison and returns false when the URL can't be parsed. request.baseURL is always set from client.baseApiUrl (executeSteps/index.ts:67), so the ! assertions are safe.
  • A rejected endpoint fails right away instead of timing out. Both resolveApiUrl calls sit outside the try/catch that swallows errors and retries. In the receipt race, confirmationPromise is either awaited (transaction.ts:537) or never reaches the loop, because waitingForConfirmation is set to false first, so there's no unhandled rejection.
  • Relative proxies like /api/relay keep working. Both the base and the endpoint resolve against window.location.origin, so they end up on the same origin.

@github-actions

github-actions Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @ted-uneven's task in 26s —— View job


📝 Docs Drift Check — Confidence: MEDIUM

What changed: The SDK now resolves step endpoints from API responses against the configured baseApiUrl origin. Executions fail if an endpoint resolves to a different host, and the API key goes only to the actual Relay API origins. The baseApiUrl option description now also says "Only set this to a proxy you control."

Docs impact: The createClient reference describes baseApiUrl but doesn't mention the new origin constraint or warn that it must point at an origin you control.

Affected pages (paths inside relayprotocol/relay-docs):

  • references/relay-kit/sdk/createClient.mdx — update

Draft:

> Replace section: `## Options` (keep the existing `typescript` example below the table unchanged)

## Options

| Property                            | Description                                                                                                                                     | Required |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| **baseApiUrl**                      | The base API url to use when making requests. You can use the `MAINNET_RELAY_API` or the `TESTNET_RELAY_API` constants exported by the package, or pass in your own proxy API url to protect your API key on the client. Only set this to the Relay API or a proxy you control. | ✅      |
| **apiKey**                          | A Relay API key to use when making requests. Only pass in this property when the Relay client instance is used exclusively server-side. Otherwise you'll leak your key to the client.                                                                                                  | ❌       |
| **chains**                          | A list of Relay Chains, by default a list of backup chains is configured                                                                       | ❌       |
| **source**                          | The source to associate your onchain activity with, should map to a domain                                                                     | ❌       |
| **logLevel**                        | The level at which to log, refer to the `LogLevel` enum exported by the package                                                                 | ❌       |
| **pollingInterval**                 | How often to poll status and check apis, this value is in milliseconds                                                                          | ❌       |
| **maxPollingAttemptsBeforeTimeout** | The source to associate your onchain activity with, should be set to a domain                                                                     | ❌       |

<Warning>
Only set **`baseApiUrl`** to the Relay API or a proxy you control. The SDK resolves the endpoints returned in API responses against this origin and rejects any that resolve to a different host, so your API key is only ever sent to the configured origin.
</Warning>

Needs human input:

  • The public surface hasn't changed since the earlier check, and draft PR docs(drift): draft from relayprotocol/relay-kit#1165 relay-docs#536 already contains this content. You don't need to run /publish-docs-pr again unless you want a new draft.
  • Please confirm where the <Warning> sits under the options table. Also decide whether references/relay-kit/sdk/installation.mdx, where proxy baseApiUrl is first introduced, needs a one-line pointer. I left that page unchanged.
  • Separately: the existing maxPollingAttemptsBeforeTimeout description looks like it was copied from source by mistake. This PR didn't cause it, so I left it alone.

To publish as a draft PR in relayprotocol/relay-docs, reply on this PR with:

/publish-docs-pr

A separate workflow will open a draft PR attributed to you. Edit the draft freely before marking it ready for review.

@ted-uneven
ted-uneven merged commit 7ff99e9 into main Nov 10, 2026
9 checks passed
@ted-uneven
ted-uneven deleted the ted/int2-1688-vulnerability-report-sdk-executes-unbound-api-steps branch November 10, 2026 16:12

This branch was successfully deployed

1 active deployment
Preview — 16b94014 Deployed Nov 10, 2026 by vercel[bot]
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