Private Input Unchecked
Detects Noir private (witness) inputs of type Field that are not referenced in any assertion, allowing the prover to set them to arbitrary values.
Private Input Unchecked
Overview
Remediation Guide: How to Fix Unchecked Private Inputs
The private input unchecked detector identifies Noir functions where private (witness) parameters of type Field are not referenced in any assert or constrain statement. In Noir, private inputs are witness values provided by the prover. Without constraints binding them, the prover can set these values to anything and still produce a valid proof.
This is the Noir equivalent of Circom’s under-constrained signal vulnerability. The difference is in the language model: Circom uses signal assignments and explicit constraints (===), while Noir uses assertions (assert, assert_eq) within constrained functions to bind witness values.
Why This Is an Issue
Private inputs represent the prover’s secret witness. The verifier never sees these values directly — it only verifies that the proof satisfies the circuit’s constraints. If a private Field parameter does not appear in any assertion, there is no constraint preventing the prover from setting it to any value in the field.
Consider a function that takes a private secret and a public hash, intended to prove knowledge of a preimage. If secret is never constrained against hash, the prover can submit any value for secret and claim to know the preimage. The proof verifies, but the claim is false.
How to Resolve
Add an assertion that constrains the private input’s value relative to other circuit values.
// Before: Vulnerable -- private input has no assertion
fn main(public_hash: pub Field, secret: Field) {
let computed = std::hash::pedersen([secret]);
// BUG: computed is never checked against public_hash
}
// After: Fixed -- private input is constrained via assertion
fn main(public_hash: pub Field, secret: Field) {
let computed = std::hash::pedersen([secret]);
assert(computed == public_hash); // binds secret to public_hash
}
Examples
Vulnerable Code
fn main(x: pub Field, secret: Field) {
let z = x + secret;
// No assertion -- secret can be any value
// The prover controls z entirely
}
fn verify_membership(root: pub Field, leaf: Field, path: [Field; 10]) {
let computed_root = compute_merkle_root(leaf, path);
// BUG: computed_root is never checked against root
// leaf and path are unconstrained witness values
}
Fixed Code
fn main(x: pub Field, secret: Field) {
assert(secret != 0); // basic constraint on secret
let z = x + secret;
assert(z == expected_output(x));
}
fn verify_membership(root: pub Field, leaf: Field, path: [Field; 10]) {
let computed_root = compute_merkle_root(leaf, path);
assert(computed_root == root); // constrains leaf and path
}
Sample Sigvex Output
CRITICAL private-input-unchecked
Private input `secret` in function `main` has no assertion constraint at line 1
Private parameter `secret` (type `Field`) in function `main` is a witness value
provided by the prover. It does not appear in any `assert` or `constrain`
statement, meaning the prover can set it to any value and still produce a valid
proof. This is a critical under-constraint vulnerability.
Function: main
Signal: secret
Confidence: 0.80
Recommendation: Add an assertion that constrains this private input. For example:
`assert(private_input == hash(public_input));`
or use it in a constraining expression.
Detection Methodology
The detector scans each constrained function (not marked unconstrained) for private Field parameters. For each function:
- Identify private Field parameters: parameters not marked
pubwhose type isFieldor containsField(e.g.,[Field; N]). - Collect assertion text: concatenate all
assert,assert_eq, andconstrainexpressions in the function body. - Check references: if a private Field parameter’s name does not appear in any assertion expression, it is flagged.
The detector only checks constrained functions because unconstrained functions execute outside the circuit and are not expected to contain assertions. Only Field-type parameters are checked because they represent arbitrary finite field elements — smaller integer types (u8, u32, etc.) have implicit range constraints from their type.
Limitations
- Name-based matching: the detector checks if the parameter name appears as a substring in assertion expressions. Aliasing (e.g.,
let alias = secret; assert(alias != 0);) is not tracked, which may cause false positives. - Does not check non-Field private inputs. Private
u32orboolinputs have implicit range constraints but may still lack semantic constraints. - Does not verify assertion sufficiency. An assertion like
assert(secret == secret)is recognized but provides no actual constraint. - Confidence is 0.80, reflecting the substring-matching limitation.
Related Detectors
- under-constrained-signal — Circom equivalent for signals without constraints
- unconstrained-public-input — Circom public inputs not bound by constraints
- unconstrained-output — output signals without constraints