\\\\\\
Feb 17, 2025
Insights
Author
Galxe
In the past few years, zero-knowledge proofs have become a star in the web3 industry, especially among protocols that put security and privacy at the forefront. While developers are still refining the rough edges of this technical concept, its implementation has elevated the standards across the industry.
During the security reviews of Galxe Identity Protocol, we uncovered an arguably critical vulnerability in the snarkjs library—a JavaScript implementation of zkSNARK schemes and the most popular ZKP library used in production. Characterized as a loophole, this security vulnerability may introduce unforeseen risks and threats to protocols, which can result in the exploitation of funds or data by malicious parties.
In this article, we’ll be dissecting the root causes of this loophole and how Galxe has implemented a solution to the snarkjs library.
Understanding snarkjs
Zero-knowledge proofs (ZKPs) rely on a Proof System algorithm, a structured set of computations that generates a proof verifying the authenticity of a statement. This allows a verifier to confirm its validity without needing access to the underlying data. However, the security of a ZKP-based system isn’t just dependent on the cryptographic principles—it also hinges on the way the system is implemented.
At the core of this implementation is circuit programming, a specialized form of proof code. Much like hardware circuits, ZKP circuits enforce strict constraints: no recursion, no complex loops, and a fixed number of iterations that must be predetermined. This design choice prevents inefficiencies and excessive computation but also introduces potential limitations and risks.
A widely adopted library for implementing ZKPs is snarkjs, which is used in production by protocols such as Tornado Cash, Semaphore, and Dark Forest. It’s favored for two key reasons:
Proven Reliability: Many privacy-focused protocols rely on it, making it the most battle-tested option in production.
Browser-Based Proof Generation: Unlike some alternatives, snarkjs supports in-browser proof generation, which is crucial for maintaining user privacy by keeping computations local rather than relying on external servers.
However, despite its advantages, the constraints of circuit programming and the reliance on specific proof libraries like snarkjs introduce a critical loophole—one that could be exploited if not properly addressed.
The snarkjs Vulnerability
In our exploration of zero-knowledge proof (ZKP) systems, we identified a subtle yet critical vulnerability within the widely-used snarkjs library. This issue is particularly insidious because it stems from a single-character typographical error—a minor mistake that can easily go unnoticed. Given snarkjs's extensive adoption in projects like Tornado Cash, Semaphore, and Dark Forest, developers often place implicit trust in its reliability, potentially overlooking the need for thorough verification.
This vulnerability is a manifestation of the input aliasing problem, previously documented in various ZKP implementations. In Solidity smart contracts, a proof’s public input X (X is of Fq) is stored using the uint256 type, which can represent values larger than q. This results in multiple integers mapping to the same Fq value after the modulo operation, a phenomenon known as "Input Aliasing." For example, s, s+q, and s+2q all represent the same point. Since q is the order of the cyclic group, any integer formed by adding multiples of q still satisfies verification. Within uint256, up to uint256_max/q distinct integers can correspond to the same point, meaning a proof set can have up to five matching hash values that pass contract verification. This means that a user can generate a proof with one set of values and have it mistakenly accepted as valid for another, potentially leading to financial or identity-based exploits.
Snarkjs generates incorrect Groth16Verifier contracts that fail to correctly validate that public signals fall within the expected scalar field's range. Specifically, the incorrect code checks whether public signals are less than a constant variable called q in the code (the base field size) instead of the constant variable r in the code (the scalar field size). This incorrect range validation allows certain values to be interpreted incorrectly.
The potential risks include:
Aliased Proofs: Attackers can generate proofs that appear valid but are in fact fraudulent.
Double Spending: In asset redemption scenarios, such as those in Tornado Cash, this flaw could allow malicious users to withdraw funds multiple times.
Compromised Identity Proofs: Systems relying on numerical proofs, like age or score verifications, could be deceived by fabricated data.
To illustrate, consider a ZKP-based system designed to verify a user’s balance of $100. Due to this vulnerability, an attacker could craft an aliased proof that falsely claims ownership of $100 + R, where R is an astronomically large number. Since the verification contract does not properly validate aliased inputs, the proof gets accepted, allowing the attacker to withdraw significantly more than they actually own.
This issue specifically affects public signals with numerical values less than q - r, where:
q−r=147946756881789318990833708069417712966
Given the vastness of this range, numerous real-world applications could be vulnerable.
For example, consider the following Circom circuit intended to verify whether an input is less than 100:
In this circuit, the verifyProof function generates a Solidity code that incorrectly returns true for both 1 and 1 + r as public inputs. Consequently, an attacker could construct an aliased proof where 1 + 21888242871839275222246405745257275088548364400416034343698204186575808495617 is treated as less than 100, effectively breaking the proof verification logic.
This vulnerability serves as a gateway for malicious actors to forge proofs, leading to:
Duplicated Transactions: Exploiting asset redemption protocols.
Sybil Attacks: Enabling the creation of multiple fraudulent identities.
Unquantifiable Financial Exposure: The potential losses become increasingly difficult to assess over time.
Even Semaphore v4, a prominent project under Ethereum’s Privacy & Scaling Explorations (PSE), was affected by a similar category of bugs.
snarkjs Vulnerability Fix
Recognizing the severity of this vulnerability, we submitted a pull request to address the issue before its official release. The fix has since been merged into the latest update of snarkjs, mitigating the specific loophole we identified. However, the extent of exposure varies based on how each project utilizes snarkjs and implements its proof verification logic. Protocols that rely on the library should conduct thorough reviews to ensure they are not susceptible to similar risks.
While we submitted a fix ahead of its release, this incident highlights a broader security concern: even widely used cryptographic tools can contain subtle flaws that go unnoticed for years. Projects using snarkjs to generate their Solidity verifier contracts must remain vigilant, as this vulnerability demonstrates how even minor oversights in proof verification can have far-reaching consequences.
Chief Technology Officer at Galxe, Yumin Xia, added: “Security is commonly likened to a barrel, with the overall system's robustness dependent on its weakest link. In ZK applications, an unprecedented level of detailed security scrutiny is essential. Any weaknesses in the ZK algorithms, circuit design, application architecture, or engineering practices can result in catastrophic consequences on production.”
About Galxe
Galxe is a decentralized web3 super app with over 31 million users worldwide. Powered by Gravity, a high-performance, EVM-compatible Layer 1 blockchain, Galxe offers a robust ecosystem of innovative products. These include Galxe Quest, Passport, Score, Earndrop, and Alva, enabling community engagement, identity verification, token distribution, and real-time, AI-powered crypto insights.
About Gravity
Gravity is a high-performance Layer 1 blockchain built for mass adoption, powering an ecosystem of over 25 million users. Built by Galxe, Gravity achieves over one gigagas per second throughput and sub-second finality with a pipelined AptosBFT consensus engine and Grevm (Gravity EVM), a parallel EVM runtime, while maintaining PoS security through restaking technology.