coinbase / coinbase/onchainkit

OnchainKit's default paymaster URL embeds the public API key, and the SDK hands that URL to every connected wallet; sponsorship bounds live only in server policy the quickstart never mentions

Open
#2,674 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
1k
Forks
520
Avg merge
32m
Merged PRs (30d)
2

Description

Repo: coinbase/onchainkit
Pin: e1b37c5530865ea5a18e33b56e966fdb63c1b3f2 (git); npm @coinbase/onchainkit@1.1.2 (the code path audited is identical in both)
PUBLIC-READY
KNOWN genus (free-settlement oracle / possession-equals-spend on the sponsor's gas)

## Summary, in plain language

OnchainKit's provider builds a default paymaster endpoint from the API key you pass it:

https://api.developer.coinbase.com/rpc/v1/base/

The quickstart templates put that key in NEXT_PUBLIC_ONCHAINKIT_API_KEY and feed it straight into OnchainKitProvider, so by construction it ships in every visitor's browser bundle. That much is by design: CDP documents the Client API Key as "safe to include in client-side code" for JSON-RPC.

The problem is what else the same string does. OnchainKit uses the identical URL as the default paymaster service. When a dapp marks a transaction as sponsored, the SDK attaches that URL as the paymasterService capability and delivers it to whatever wallet the user connected, which then calls it to get gas sponsored. Possession of the URL is the only credential in this flow: no signature from the dapp, no per-call bound, no allowlist expressed anywhere on the client. Anyone who has the public key (any visitor) can present sponsorship requests to the project's paymaster endpoint.

CDP's own paymaster documentation says sponsorship is governed by "policy controls" (contract allowlists, per-user limits, global spend caps) and that a contract allowlist "protects against unintended sponsorship". Nothing in OnchainKit, in the templates, or in the provider configuration surface mentions these policies. A developer following the quickstart ships a sponsorship credential that is public by construction and whose only bounds are server-side settings they were never told to configure. The result is the familiar shape: the URL is a settlement oracle, and the project pays the gas bill (plus the documented 7% fee) for whoever quotes the public string.

I did not and could not verify what an unconfigured project's paymaster policy allows, because that is server-side state; no Coinbase endpoint was contacted during this audit.

## Reproduction

PoC: poc/ck01-paymaster-url-possession.mjs
Evidence: evidence/ck01-paymaster-url-possession.txt
Run: ./REPRO.sh (runs the PoC twice; outputs are byte-identical)

The PoC renders the real published OnchainKitProvider from @coinbase/onchainkit@1.1.2 with a dummy key (ckbl_r04p1_dummy_public_key) and the Base chain, reads back useOnchainKit().config.paymaster, and asserts:

1. the default paymaster URL is the CDP rpc endpoint with the API key embedded;
2. the SDK's own getPaymasterUrl extracts exactly that URL from the paymasterService capability the Transaction provider builds;
3. the pinned template declares NEXT_PUBLIC_ONCHAINKIT_API_KEY and passes it as the provider's apiKey.

No network is used. The wallet-handoff itself (capabilities.paymasterService.url on wallet_sendCalls) is the code path in TransactionProvider.tsx lines 129 to 137 and CheckoutProvider.tsx around line 344, cited rather than executed, since driving a live wallet connector adds nothing to the value being proven (the capability object is constructed from the same config value the PoC executes).

## Impact

For every quickstart-derived deployment that enables sponsored transactions without separately configuring paymaster policies, spend authority over the sponsor's gas is a public string. The blast radius is whatever the server-side policy permits, which defaults to unknown and unmentioned. This is the same possession-equals-spend genus as a payment webhook URL that authorizes settlement on contact.

## Suggested fix

Separate the sponsorship credential from the public RPC key: make the paymaster a distinct, explicitly-configured endpoint (server-proxied if it must stay public) instead of deriving it from the client API key by default. When sponsorship is enabled but no policy surface has been touched, say so loudly in the provider (a console warning referencing the policy controls), and document the allowlist and cap steps inside the quickstart template rather than only in the paymaster product docs.

Contributor guide

No contributing guide indexed for this repository

Research direction

Run ./REPRO.sh and read poc/ck01-paymaster-url-possession.mjs with evidence/ck01-paymaster-url-possession.txt. Then inspect TransactionProvider.tsx lines 129–137, CheckoutProvider.tsx around line 344, and the quickstart template's NEXT_PUBLIC_ONCHAINKIT_API_KEY wiring. Done means the public API key is no longer reused as the default sponsorship credential, policy guidance is surfaced in the provider and quickstart, and the reproduced behavior is covered.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
documentation, security, web-dev
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.