Skip to content
Quantix Security Labs
Primer

Your RPC endpoint is part of your attack surface

Frontends and backends trust the node they query. A review of what an untrusted or misconfigured RPC layer can do to an otherwise sound application.

Written by
Quantix Research, Infrastructure Security
Published
Reading time
7 minutes
Category
Blockchain Infrastructure

Primer. Explains a known class of issue. Code is simplified and illustrative, not for production use.

Illustration: clients reaching a blockchain node through RPC endpoints arranged in a ring, with one connection passing through an interceptor.
Contents
  1. What applications trust the node for
  2. When the answer is wrong
  3. Network identity
  4. Reorgs and finality
  5. Exposed nodes and leaked keys
  6. The write path
  7. Hardening the RPC layer
  8. Conclusions
  9. Recommendations
  10. Bibliography

Smart contracts get most of the attention in Web3 security reviews, and for good reason: they hold the funds. But almost every user interaction with a contract passes through a JSON-RPC endpoint first. The frontend asks a node for balances and allowances, simulates a transaction against it, estimates gas with it, and finally hands it the signed transaction to broadcast. Backends index events through the same interface.

Each of those steps trusts the node to tell the truth and to be available. This primer looks at what happens when it does not, and at the controls that keep a misbehaving or exposed RPC layer from turning into a loss.

What applications trust the node for

A typical application does not verify chain state itself. It does not check block headers against consensus or validate Merkle proofs for the storage it reads. It accepts whatever the provider returns. That is a reasonable engineering trade-off, but it should be a conscious one, documented in the threat model alongside the smart contracts.

ComponentTrusts the RPC forIf the answer is wrong
Wallet and dApp displayBalances, allowances, token metadataUsers decide, approve and sign based on false information.
Transaction simulationeth_call and eth_estimateGas resultsA harmful transaction can be shown as harmless before the user signs.
Transaction constructionNonces, fee estimates, chain IDStuck or replaced transactions, overpayment, or signing for the wrong network.
Backend indexersLogs, receipts, block dataCredits deposits that never happened or were reorganised away.
Transaction submissioneth_sendRawTransactionDelayed inclusion, censorship, or exposure to front-running.
Where applications rely on RPC responses, and what an incorrect response causes
The trust chain between an application and the blockchainAn application talks to an RPC provider, which talks to a node, which follows the network. Each hop can return wrong information: the provider can return false state or withhold transactions, the node can lag or sit on a fork, and the network can reorganise blocks before finality. Each boundary is a trust assumption unless responses are verified.Frontend, walletbuilds transactionsshows balances, quotesCan get wrongWrong calldata builtfrom a bad response1RPC providereth_call, getLogs,estimateGas, sendRawCan get wrongReturns false stateWithholds or delays txs2Nodeexecutes, stores statefollows a chain headCan get wrongLags the headSits on a minority fork3Network, consensusorders and finalisesblocksCan get wrongReorgs before finalityEach numbered boundary is a trust assumption unless the response is checked:state proofs (eth_getProof), more than one independent provider, finality-aware reads, and simulation before signing.
Each hop between an application and consensus is a trust assumption unless the response is verified.

When the answer is wrong

An RPC response can be wrong for innocent reasons, such as a node that is out of sync or a load balancer that routes consecutive requests to nodes at different heights, or for hostile ones: a compromised provider, a malicious public endpoint added by a user, or a network attacker who can substitute the endpoint a client connects to.

The impact depends on what the application does with the answer. Displaying a stale balance is an annoyance. Showing a simulation that hides a token drain, or crediting an off-chain account for an on-chain deposit that does not exist, is a loss. The guiding question for review is simple: for each RPC call, what decision does the result drive, and what is the worst outcome if the result is false?

Where the decision is consequential, applications can corroborate. Reading the same value from two independent providers at the same block makes a single lying provider detectable. Pinning reads to a specific block hash, rather than latest, ensures both providers answer about the same state.

verified-read.ts (illustrative)ts
// Illustrative: read critical state from two independent providers// at the same finalized block, and refuse to act if they disagree.async function rpc(url: string, method: string, params: unknown[]) {  const res = await fetch(url, {    method: "POST",    headers: { "content-type": "application/json" },    body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),  })  const { result, error } = await res.json()  if (error) throw new Error(`${method}: ${error.message}`)  return result} export async function verifiedBalance(address: string): Promise<bigint> {  const block = await rpc(PRIMARY_RPC, "eth_getBlockByNumber", ["finalized", false])  const at = { blockHash: block.hash }                     // EIP-1898 block reference  const [a, b] = await Promise.all([    rpc(PRIMARY_RPC, "eth_getBalance", [address, at]),    rpc(SECONDARY_RPC, "eth_getBalance", [address, at]),  ])  if (a !== b) throw new Error("Providers disagree at the same block")  return BigInt(a)}

Simulation deserves particular care. Wallets and dApps increasingly show users the predicted effect of a transaction before they sign: tokens out, tokens in, approvals granted. That preview is only as honest as the node that produced it. A malicious endpoint can return a benign simulation for a harmful transaction, and some contracts behave differently when they detect that they are being simulated rather than executed, for example by checking block properties that differ between the two. Previews should come from infrastructure the application controls or trusts, and should be presented as a prediction rather than a guarantee.

Network identity

Every Ethereum-style transaction signature includes a chain ID, introduced by EIP-155 so that a transaction signed for one network cannot be replayed on another. That protection only helps if the chain ID is the one the user intended. Applications should read the chain ID from the connected provider, compare it with the network they expect, and refuse to construct or request signatures when the two differ.

The same applies to off-chain signatures. Typed-data domains carry a chain ID and a verifying contract; an application that builds the domain from the provider’s answer, rather than from its own configuration, lets a misconfigured or malicious endpoint steer users into signing for the wrong network or contract. Configuration that defines which chains and contracts an application works with should ship with the application and be treated as security-sensitive code.

Reorgs and finality

The latest block is not settled. Blocks near the head of the chain can be replaced, and with them the events a backend has already processed. Since the move to proof of stake, Ethereum’s JSON-RPC accepts safe and finalized as block parameters, which let applications read state that is far less likely, or under normal operation not expected, to be reverted.

Exposed nodes and leaked keys

Teams that run their own nodes inherit a second problem: the node’s administrative surface. Execution clients offer namespaces beyond the standard eth methods, for administration, debugging and transaction pool inspection. These are useful internally and dangerous when reachable from the internet. Older client versions also exposed account-management methods that could unlock keys held by the node; modern clients have deprecated or removed them, but legacy deployments persist.

node flags (illustrative)bash
# Illustrative go-ethereum flags for an application-facing node.# Bind to a private interface and expose only the namespaces the app needs.geth \  --http --http.addr 10.0.0.12 --http.port 8545 \  --http.api eth,net,web3 \  --http.vhosts rpc.internal.example \  --http.corsdomain "https://app.example" # Never expose admin, debug, txpool or legacy account namespaces# to an untrusted network. Put authentication and rate limits in front.

Hosted providers shift that burden but introduce another: API keys embedded in frontend bundles. Anyone can read them. At best a leaked key lets others consume your quota and raise your bill; at worst, if the same key grants access to privileged methods or account features, it becomes a credential. Keys used in browsers should be restricted by origin and method at the provider, and separate from keys used by backends.

The write path

Broadcasting a transaction through a public endpoint exposes it to everyone watching the public mempool before it is included in a block. For trades and liquidations that is an invitation to front-running and sandwich attacks. Private submission services that forward transactions directly to block builders reduce that exposure, at the cost of trusting the service. Either way, contracts should not depend on transaction privacy for safety: slippage limits and deadlines belong in the transaction itself.

Availability is part of security as well. A protocol whose keepers, liquidators or guardians submit through a single provider can be stalled by that provider’s outage at exactly the wrong moment. Critical automation needs redundant endpoints and alerting when submissions fail.

Hardening the RPC layer

  • List every RPC call the application makes and the decision each one drives; treat the list as part of the threat model.
  • Corroborate consequential reads across independent providers, pinned to the same block.
  • Credit deposits and trigger irreversible actions only from finalized data.
  • Ship a pinned, integrity-checked list of endpoints and chain IDs; never fetch them over plain HTTP.
  • Expose only the namespaces an application needs, on private interfaces, behind authentication and rate limits.
  • Restrict browser-embedded API keys by origin and method; keep backend keys separate.
  • Use redundant endpoints for keepers and guardians, with alerts on submission failures.

The choice between running nodes and buying access is not a choice between risk and safety. Self-hosting removes a third party from the trust path but adds patching, configuration and monitoring work that teams frequently underestimate. Hosted access is simpler to operate but concentrates trust and availability in a provider whose incidents become yours. Many teams settle on a hybrid: hosted providers for user-facing reads, a self-operated node for the reads that drive irreversible actions, and cross-checks between them. Whatever the arrangement, it should be written down, reviewed and tested like any other security control.

None of this replaces a sound contract. It makes sure that the decisions users and services take on the way to that contract are based on the same truth the contract will see.

Conclusions

An application is only as correct as the data it acts on, and in most stacks that data arrives unverified from an RPC provider. Wrong answers, lagging heads, reorganised blocks, exposed administrative interfaces and a single submission path are all ways an otherwise sound application can fail.

The controls are well understood and mostly operational: corroborate consequential reads, act on finalized data, pin network identity, minimise the exposed surface and make submission redundant.

Recommendations

Application teams

  1. List every RPC call and the decision it drives, and corroborate the consequential ones across two providers pinned to the same block hash.
  2. Trigger irreversible actions, such as crediting deposits, only from finalized data.
  3. Read the chain ID from the provider and refuse to sign when it differs from the expected network.

Infrastructure and operations

  1. Expose only the namespaces each consumer needs, on private interfaces, behind authentication and rate limits.
  2. Restrict browser-embedded API keys by origin and method, and give keepers redundant endpoints with failure alerts.

Reviewers

  1. Include the RPC, simulation and submission paths in the threat model, not only the contracts.
  2. Test the application against a node that returns stale or inconsistent answers.

Bibliography

Primary sources: standards, official documentation, incident reports and papers.

  1. 1JSON-RPC API(opens in a new tab)ethereum.org developer documentation
  2. 2JSON-RPC Server(opens in a new tab)go-ethereum documentation
  3. 3EIP-1898: Add blockHash to defaultBlock methods(opens in a new tab)Ethereum Improvement Proposals
  4. 4EIP-155: Simple replay attack protection(opens in a new tab)Ethereum Improvement Proposals
  5. 5Ethereum execution APIs specification(opens in a new tab)ethereum/execution-apis on GitHub

Applied on this site

Assessments, case studies and tools that apply this publication.

Want this question asked of your code?

Quantix reviewers apply the same reasoning to production systems. Share what you are building and when it ships.