Can an attacker slip past your transaction policies?

It all comes down to how they’re enforced

Intro

Complete mediation ensures that all policy checks succeed before a key produces a signature—but not all systems get it right.

In this article

When you’re using a wallet, a key manager, or an asset management system, policies are what keep you safe from mistakes and attackers. A policy might say “this key can only sign transactions under 1 ETH over the course of 5 days, and only to one of these five addresses”; if you mistype—or if an attacker gains access to your system—the policy limits (or even prevents) damage. 

Most key managers offer a policy engine that lets users protect funds by attaching rules to keys. If that policy engine can be tricked, though—by exploiting everything from programmer error to a compromised deployment process—an attacker can coax the signer into signing a malicious (and non-policy-compliant!) transaction. This post explains how to tell if your signing pipeline leaves exploitable gaps for attackers. 

Location, location, location: enforce policies in the right place

A transaction policy is only as strong as its proximity to the signing operation: the farther away the check runs, the more surface area an attacker has to skirt it. 

Weakest option: front-end checking 

Some wallets enforce policies on the frontend, and then sign transactions on the backend. The recent ~$1.5B Bybit hack is a good example of why this approach to policy enforcement is broken. Hackers compromised a Safe wallet developer, and published malicious frontend code that displayed transactions incorrectly for Bybit employees—making a devastating transaction appear completely routine. Similar attacks would completely bypass policy enforcement: all the attackers have to do is remove a few policy checks and publish the new frontend code. Since the back-end doesn’t do any policy checking, it will sign any transaction from authorized users.

Better option: back-end checking

When wallets enforce policies on the back-end, they avoid the problem we discussed above. There are no policy checks to delete on the front-end side—and even if attackers hack the UI to display a different transaction than the one submitted for signing, the policy checks on the back-end still have a chance to prevent the signing.

Back-end checks fall apart, though, when they’re not implemented correctly. Consider a system that, for every signing code path, checks if (policy_ok) { sign(); }. This approach is error-prone. One missing check is enough to bypass policy enforcement altogether; each additional signing endpoint is another chance to miss a critical policy check—all it takes is a developer forgetting a single line of code.  

Best option: type-level enforcement on the back-end 

This approach uses a programming language’s type system to make it impossible for programmers to forget critical back-end policy checks. Type systems let programmers label data with types (e.g., number or letter), and prevent errors by looking for type incompatibilities (e.g., adding a number to a letter). Compilers—the software that turns programs into machine code—run type checking to look for type errors; if the compiler detects a type error, it doesn’t produce machine code. This means that type systems can completely prevent certain classes of bugs. 

We use the type system to make sure that CubeSigner can’t produce a signature unless policies approve. We have two types, one for keys, and one for  the { key, transaction_data } pair; signing functions only accept the second type, which in turn can only be created by checking that the policies hold for a given key and transaction pair. Code that requests a signature without first checking policies simply won’t compile. 

This “policy-safety” is similar to things like memory safety. Memory safe languages use the type system to prevent programmers from accidentally using invalid pieces of memory—one of the most critical kinds of bugs. Similarly, we use the type system to prevent unchecked policies by construction.

Takeaways

It should be impossible to sign a transaction without first checking the relevant policies. Rely on compile‑time guarantees, not developer vigilance—if code cannot build without satisfying every required policy, entire classes of runtime oversights simply disappear. And remember: front‑end‑level validation is user experience, not security.

About

Blog & Updates

Explore Related Blog Posts