Private Input Unchecked Remediation
How to fix unchecked private inputs in Noir by adding assertions that constrain witness values.
Private Input Unchecked Remediation
Overview
Related Detector: Private Input Unchecked
Private inputs in Noir are witness values provided by the prover. Without assertions constraining them, the prover can set these values to anything and still generate a valid proof. The fix is to add assert or assert_eq statements that bind the private input to the circuit’s intended computation.
Recommended Fix
Before (Vulnerable)
fn main(public_hash: pub Field, preimage: Field) {
let computed = std::hash::pedersen([preimage]);
// BUG: no assertion -- preimage is unconstrained
// The prover can claim to know any preimage
}
After (Fixed)
fn main(public_hash: pub Field, preimage: Field) {
let computed = std::hash::pedersen([preimage]);
assert(computed == public_hash);
// preimage is now constrained: it must hash to public_hash
}
The assertion generates a constraint in the circuit. The verifier checks that the Pedersen hash of the prover’s preimage equals public_hash. If the prover uses a wrong preimage, the proof will not verify.
Alternative Mitigations
1. Range Assertions
When the private input must fall within a specific range:
fn main(amount: pub Field, balance: Field) {
// Constrain: balance must be at least amount
assert(balance as u64 >= amount as u64);
// Constrain: balance is within valid range
assert(balance as u64 <= 1000000);
}
2. Equality Assertions Against Public Inputs
When the private input must match a derivation from public data:
fn main(commitment: pub Field, value: Field, blinding: Field) {
let computed_commitment = std::hash::pedersen([value, blinding]);
assert_eq(computed_commitment, commitment);
// Both value and blinding are now constrained
}
3. Use in Constrained Return
If the function returns a value that depends on the private input, and the return value is used in a parent constrained context, the private input may be transitively constrained. However, this is fragile — add an explicit assertion for clarity:
fn process(x: pub Field, witness: Field) -> Field {
assert(witness != 0); // explicit constraint
let result = x / witness;
result // returned value carries the constraint
}
Common Mistakes
Assertions on unrelated values: adding assert(1 == 1) satisfies the “has an assertion” check syntactically, but does not constrain any private input. The assertion must reference the private parameter by name.
// WRONG: assertion does not reference `secret`
fn main(x: pub Field, secret: Field) {
assert(x != 0); // only constrains x, not secret
let z = x + secret;
}
// CORRECT: assertion references `secret`
fn main(x: pub Field, secret: Field) {
assert(secret != 0);
let z = x + secret;
assert(z == x + secret); // redundant but shows intent
}
Aliasing without assertion: assigning a private input to a local variable and asserting on the variable works for the circuit constraints, but the detector uses name-based matching and may not recognize the alias. Keep assertions close to the parameter name for both clarity and detector accuracy.
// Less clear (and may trigger a false positive)
fn main(x: pub Field, secret: Field) {
let s = secret;
assert(s != 0);
}
// Clearer
fn main(x: pub Field, secret: Field) {
assert(secret != 0);
}
Confusing unconstrained and constrained context: assertions inside unconstrained functions do not generate circuit constraints. They run only during witness generation and can be bypassed by a malicious prover. Place assertions in constrained functions:
// WRONG: assertion in unconstrained context is not enforced
unconstrained fn validate(x: Field) {
assert(x != 0); // this is NOT a circuit constraint
}
// CORRECT: assertion in constrained context
fn validate(x: Field) {
assert(x != 0); // this IS a circuit constraint
}