The contracts have been audited twice, the multisig is a hardware-wallet quorum, the keys are in cold storage. None of that matters to an attacker who can change one line of JavaScript on the page your users load. The front-end is the part of the system that asks a human to sign something, and whatever it asks for, most users will approve. From an attacker's seat it is the cheapest route to funds in the stack: no exploit, no reentrancy, no governance. Just a different to address in the transaction the user was already going to send.
The incident class is well established: a registrar account phished, a DNS record pointed at a cloned site, an approval request that looked like the normal one, and days of drained wallets before anyone connected the complaints. Equally established is the statement afterwards that "the contracts were never compromised". True, and irrelevant to the people who lost money.
This article is for teams that ship a web interface to a protocol, and for the security leads who scope what gets tested: why the front-end is attacked, the mechanisms we see, the defences that change the outcome, and how we cover dApp front-ends on a penetration test.
Why the front-end is a target
Three properties make the front-end attractive.
It is the point of signing. Every value transfer begins with a transaction the UI constructs and the user approves. Control the construction and you control the transfer. The wallet is not a defence: it displays what it was handed, and an approve or permit request for a malicious spender looks, to most users, exactly like the legitimate one they signed last week.
It is the least-reviewed component. Audits and bug bounties target the contracts. The front-end is a React application maintained by a smaller team, deployed through a pipeline nobody has threat-modelled, with hundreds of dependencies not chosen for their security posture.
It is where trust is widest. The site depends on a registrar, a DNS provider, a hosting platform, a CDN, third-party scripts and a package registry. Each is a party whose compromise becomes yours, and most hold more power over your users than your multisig does.
The common attacks
DNS and registrar hijacks
The attacker does not need your servers. They need the account at your registrar or DNS provider, obtained through credential reuse, SIM-swapped two-factor codes, a social-engineered support agent, or a session token lifted from an employee's browser. They point the A or CNAME record at a host they control, which serves a copy of your site with one change. TLS is no obstacle; a valid certificate for a domain they now control takes minutes.
The copy can be byte-identical except for the contract addresses, or the real site with an injected script. Users see the real URL, a valid padlock, and the interface they know. Pre-conditions: no hardware-backed authentication on the registrar account, no registry lock, and no monitoring of DNS answers from outside your network.
Compromised hosting and CDN
Where the site is served from a hosting platform or object store, deployment credentials are the target: a CI token in a public repository, a long-lived API key on a laptop, an over-permissioned service account. A CDN in front can be reconfigured to inject a script at the edge or rewrite responses for specific paths. The domain and certificate are untouched; the only sign is a changed file hash that nobody is checking.
Supply chain through npm and analytics
A dApp bundle contains code from hundreds of packages, all running with the same privileges as your own: full access to the DOM, the wallet provider object, and every transaction the page builds. A maintainer account takeover, a typosquatted dependency, or a post-install script in a transitive package puts attacker code in production at the next build. The attacker does not target you; they target a popular package and collect from every dApp that ships it.
Third-party scripts loaded at runtime — analytics, chat widgets, error tracking, tag managers — bypass your build entirely. The script you included last year is whatever the vendor's CDN serves today. If the vendor is compromised, or its domain lapses and is re-registered, the attacker is on your page without touching your code.
Malicious contract-address swaps in configuration
The simplest effective change is in the address table: the map of chain ID to contract addresses every dApp carries. Swap the router, the vault or the token spender for an attacker address and every interaction routes funds to them. approve requests go to a malicious spender; deposits go to a look-alike vault; the UI shows success because the attacker's contract returns whatever the UI expects.
This survives code review when it happens outside the repository — an environment variable, a deployment-time config fetch, a feature-flag service — and survives testing because test environments use a different chain ID.
RPC endpoint and wallet-connect tampering
The front-end reads balances, allowances and prices through an RPC URL. An attacker who controls that endpoint — by changing the configured URL or compromising a third-party provider — controls what the UI believes: a fake balance, a hidden drain, or a crafted eth_call result that makes a malicious transaction simulate as routine.
Wallet-connection relays that carry session requests between the dApp and a mobile wallet are a man-in-the-middle by design. A compromised relay, or a UI that does not verify the connected chain and account, lets an attacker substitute requests or leave the user signing on the wrong network.
XSS leading to transaction substitution
Classic web vulnerabilities do not go away because the back-end is a blockchain. A cross-site-scripting bug — a token name rendered unescaped, a URL parameter echoed into the page, a Markdown field in a governance proposal — executes in the dApp's context. From there the attacker reaches window.ethereum, hooks the request method, and rewrites the to and data fields of the next transaction. The UI shows the intended swap; the wallet receives an approval to drain the user. Token lists and user-generated content are the usual carriers, because they arrive from outside and are rendered as if trusted.
The signature the user did not understand
Even on a legitimate page, the UI decides how a signature request is framed. Off-chain signatures — permit, order signing, eth_signTypedData payloads for intent-based systems — can authorise large transfers with no on-chain transaction visible at signing time. A page that asks for blanket approvals, or for signatures with vague typed-data fields, is training users to approve things they cannot read. When the page is later compromised, that training is what the attacker exploits.
Defences that matter
Lock the control plane
- Registrar and DNS. Hardware-backed MFA on every account that can change a record, registry lock where offered, DNSSEC, and CAA records limiting who can issue certificates for your domain. Monitor DNS answers and certificate transparency logs from outside your infrastructure.
- Deployment. Short-lived, scoped CI credentials; no long-lived tokens on laptops; a separate identity for production publishes; review of the pipeline configuration, not just the application code.
- Dependencies. Pin exact versions, commit the lockfile, build from it only, and run installs with scripts disabled where possible. Review every new dependency as code, because it is. Keep runtime third-party scripts to a minimum and host what you can yourself.
Constrain what the page can do
Content Security Policy is the most under-used defence in Web3. A strict CSP with no unsafe-inline, nonces or hashes for your own scripts, and explicit script-src and connect-src allowlists means an injected script cannot run and the page cannot talk to an RPC endpoint you did not name. It turns many XSS findings from critical to blocked. Pair it with Subresource Integrity on every external script and stylesheet, so a CDN serving a modified file causes a load failure rather than a silent compromise. SRI requires the file to be immutable, which is the point: a script that changes under you is a script you did not review.
Build the site as a static bundle with a reproducible build and publish the hash, so anyone can verify that what the CDN serves is what the pipeline produced.
Verify the front-end against the chain
The page should show the contract address it is about to interact with, so the user can compare it with the addresses in your documentation, your ENS records, or a verified on-chain registry. A UI that hides the address behind a friendly label gives the user nothing to check. Resolve addresses from an on-chain source where possible, so a configuration swap in the bundle disagrees visibly with the chain.
Simulate every transaction before presenting it. Decode the calldata against the known ABI and render it in plain language: which function, which recipient, which amount, which spender. Show the simulated balance changes. If the decoded transaction does not match the action the user chose — a swap that turns out to be an approval — refuse to present it. This is the control that would have stopped most front-end drains, and it protects against compromises you have not thought of, because it compares intent with effect rather than matching a known bad pattern.
Request the minimum approval. Exact-amount approvals, permit with a short deadline, and typed-data payloads with readable fields reduce the blast radius when the page is eventually compromised anyway.
IPFS and ENS hosting: the trade-offs
Serving the front-end from IPFS, addressed through an ENS content hash, removes the registrar, DNS provider and hosting platform from the trust set in one move. Content is identified by its hash, so the bundle is the one you published, and the only way to change it is a transaction from the ENS record owner.
The trade-offs are real. Most users reach IPFS content through an HTTP gateway, which is a new trusted party that can serve anything it likes. ENS record updates need the same key-custody discipline as a contract upgrade. Pinning must be reliable or the site disappears. We recommend IPFS and ENS as an additional, verifiable channel alongside the DNS site, with the content hash published so users and monitors can compare the two.
Sign releases and watch production
Sign every build artefact and record the hash with its commit. Run a monitor that fetches the production site from several networks every few minutes, hashes the bundle and the address configuration, and alerts on any difference from the signed release. Do the same for DNS answers, certificate transparency entries and the third-party scripts you load. The goal is to learn of a compromise from your own alerting rather than from a user on social media; ten minutes versus ten hours of exposure is the difference between an incident and a catastrophe. Have the runbook ready — who reverts DNS, who pulls the CDN config, who posts the warning — and rehearse it.
Treat the UI as in-scope
Most protocols scope security as "the contracts". The front-end is left to the web team, tested for functionality, and deployed through whatever pipeline was fastest to set up. Attackers have noticed that the perimeter of an audited protocol is not where the audit stopped.
Put the front-end in the threat model explicitly. Write down who can publish to production, what each third party can change, and what the user is asked to sign. Put the deployment pipeline, the registrar account and the dependency policy in scope for review, and the hosted dApp in scope for the bug bounty. The contracts are the vault; the front-end is the door everyone walks through. The operational-layer items in our audit checklist sit alongside this one.
How our web pentests cover dApp front-ends
A dApp front-end assessment is a web application penetration test with a Web3 threat model layered on top. We cover:
- Transaction construction. We trace every path from a user action to a wallet request, confirm that addresses, amounts and spenders come from sources the attacker cannot influence, and test whether URL parameters, token lists or on-chain metadata can alter what is built.
- Injection and script execution. Conventional XSS, prototype pollution and DOM clobbering testing, with the payload goal being wallet-provider hooking rather than cookie theft. We confirm whether the deployed CSP actually blocks execution.
- Configuration integrity. Where the address table and RPC URLs live, how they reach the bundle, and who can change them without a code review.
- Supply chain. Lockfile review, install-script audit, a map of runtime third-party scripts, SRI coverage, and the permissions of the build and deployment identities.
- Hosting and DNS posture. Registrar protections, DNSSEC, CAA, certificate issuance history, CDN configuration, and whether a monitor would detect a changed bundle.
- Signature hygiene. What the user is asked to sign, whether approvals are bounded, and whether typed-data payloads are readable.
- Wallet and RPC flows. Chain and account verification after connection, behaviour under a hostile RPC response, and wallet-connect session handling.
Findings come with a reproduction, an impact statement in terms of user funds, and a fix. We re-test fixes and recommend a re-check after any pipeline change.
Practical next steps
Put hardware MFA and a registry lock on the domain this week. Pin dependencies and commit the lockfile. Ship a strict CSP with SRI on every external script. Show the contract address and decode every transaction before the wallet sees it. Publish a build hash and stand up a monitor that compares production against it. Then scope a test that treats the front-end as the attack surface it is.
Our web and dApp pentest scope is on the services page; reach the team on Telegram.