Web2 Vulnerabilities, Web3 Risk
Web2 and Web3 security are often treated as separate domains. Labels like “Web2 Security Researcher” and “Web3 Security Researcher” create a false dichotomy — when in reality, modern DApps are deeply integrated across both layers.
Smart contracts may be secure in isolation, but DeFi’s composability introduces new, dangerous risks. Whoever controls the calldata controls the world. We’ve seen billion-dollar exploits stem from frontend misconfigurations or compromised libraries that injected malicious calldata into trusted contracts.
Why Web2 Security Matters More Than Ever
When people think about securing a DApp, they often stop at smart contracts. But the reality is that much of the DApp’s behavior is governed by what happens offchain — in backends, frontends, and CI/CD pipelines. This is the “Web2 layer” of crypto.
And it’s increasingly where attackers are looking.
Unlike smart contracts, which are immutable and reviewed line-by-line, Web2 systems are mutable, fast-moving, and rarely audited with the same scrutiny. They often hold the keys to core logic:
- Who can call sensitive contract functions
- What calldata gets passed in
- When transactions are triggered
If an attacker compromises any of those systems — by injecting fake data into a backend, modifying frontend scripts, or abusing deployment infrastructure — they can bypass the "secure" onchain logic entirely.
The issue isn’t always that smart contracts are vulnerable. It’s that everything surrounding them can make them insecure — and most teams don’t realize it until it’s too late.
Let’s examine a real-world example Guardian uncovered, where the smart contracts were sound — but the calldata passed through a Web2 backend made the DApp vulnerable.
Example of a Web2 Attack
When a user registers on this website, their data is stored in the application’s backend:
async handler(ctx, { data }) {
let account = await accountByExternalId(ctx, data.id);
if (account === null) {
const newAccount = await ctx.db.insert("accounts", {
displayName: `${data.username}`,
externalId: data.id,
profileImage: data.image_url,
preferences: {
theme: "DEFAULT",
settingA: false,
settingB: false,
showValuesInUnits: false,
},
socialHandle: maybeGetSocialHandle(data),
});
account = (await ctx.db.get(newAccount)) as Doc<"accounts">;
}
}
A cron job runs every 30 minutes to flush tokens held in user-specific “forwarder” contracts to the protocol’s treasury:
function batchFlushERC20(
address[] calldata forwarderAddresses,
address tokenContractAddress
) public {
for (uint256 i = 0; i < forwarderAddresses.length; i++) {
address forwarderAddress = forwarderAddresses[i];
Forwarder forwarder = Forwarder(forwarderAddress);
forwarder.flushERC20(tokenContractAddress);
}
}
From a Web3 security perspective, this function looks secure, and would most likely pass a review. A web3 security researcher might check edge cases like zero addresses or reentrancy — but that’s not where the real risk lies.
Here’s how the Web2 backend calls this function:
export const flushExternalDeposit = internalAction({
args: {},
handler: async (ctx) => {
const allUserIds = await ctx.runQuery(internal.deposits.allUserIds);
const forwarderAddresses: string[] = [];
for (const userId of allUserIds) {
const address = getPredeployForUser(userId, BASE_CHAIN_ID);
if (address !== "test") {
forwarderAddresses.push(address);
}
}
const queueId = await contractWrite({
contractAddress: TREASURIES[BASE_CHAIN_ID],
functionName:
"function batchFlushERC20(address[] forwarderAddresses, address tokenContractAddress)",
args: [forwarderAddresses, BASE_USDC],
abi: [
{
type: "function",
name: "batchFlushERC20",
inputs: [
{ type: "address[]", name: "forwarderAddresses" },
{ type: "address", name: "tokenContractAddress" },
],
outputs: [],
},
],
chainId: BASE_CHAIN_ID,
});
},
});
Every 30 minutes, the application fetches all users who have ever registered and passes their addresses to the batchFlushERC20
function as forwarderAddresses
.
This creates a unique problem: blockchains have a block gas limit. As more users register, the calldata grows — and so does the gas required to process each flush. Every additional user adds another external call to a forwarder contract.
Eventually, as the user base grows — or if a malicious actor spams thousands of fake registrations — the function call will exceed the block gas limit. At that point, the flushing mechanism fails entirely, denying service to a critical part of the DApp’s asset flow.
Conclusion
As DApps evolve, core logic is increasingly pushed off-chain — and attackers know it. Smart contract audits alone are no longer enough.
Guardian provides full-stack security — auditing not just your contracts, but everything connected to them. If you’re serious about shipping a secure product, we’ll help you find and fix protocol-killing issues at every layer.
Book an audit with Guardian here, to secure every layer of your stack.