Skip to main content
Sigvex

Cairo External Without Validation

Detects external, state-mutating Cairo functions that accept parameters but contain no assertions, trusting arbitrary caller-supplied input.

Cairo External Without Validation

Overview

An external, non-view function on a Starknet contract is callable by anyone. When such a function accepts parameters, mutates state, and contains no assertions at all, it is accepting unvalidated data from arbitrary callers and acting on it. This detector reports that combination: external (not view, not constructor), at least one parameter, performs a storage write or external call, and has no assert anywhere in its body.

Missing validation covers two overlapping risks. The first is missing access control — a state change that should be restricted to an owner or role but checks no caller identity. The second is missing input validation — amounts, addresses, indices, or selectors used without bounds, allowing ill-formed or out-of-range values to corrupt state. Both manifest as the same code shape: a public entry point that changes state without first asserting anything.

Why This Is an Issue

The textbook case is an ownership setter. A function named set_owner that writes the owner storage slot from its argument, with no check that the caller is the current owner, lets any address seize control of the contract. The same applies to functions that update fee parameters, pause flags, oracle addresses, or any privileged configuration: without an assert(get_caller_address() == ...) guard, the privilege is open to the public.

Input validation matters even for unprivileged functions. A transfer that does not check the recipient is non-zero can burn funds to the zero address. A function that indexes an array by an unchecked argument can read or write out of bounds. None of these require special access; they only require that the function trust its inputs, which an external function should never do.

How to Resolve

Add assertions on caller identity where the operation is privileged, and on input shape where the operation is value-bearing, before any state change.

// Vulnerable: anyone can set the owner, no input or caller check
#[external(v0)]
fn set_owner(ref self: ContractState, new_owner: ContractAddress) {
    self.owner.write(new_owner);
}
// Fixed: caller is checked and the new owner is validated
#[external(v0)]
fn set_owner(ref self: ContractState, new_owner: ContractAddress) {
    assert(get_caller_address() == self.owner.read(), 'not owner');
    assert(new_owner.is_non_zero(), 'zero owner');
    self.owner.write(new_owner);
}

Detection Methodology

The detector iterates the parsed functions and keeps only those that are external, not view, not a constructor, and take at least one parameter. For each, it checks whether any operation mutates state (a storage write or an external call). State-mutating candidates that contain no assertion are reported once, citing the function and its parameter count. Functions with any assertion are assumed to perform some validation and are skipped.

The assert check is intentionally a presence test rather than a sufficiency test: the detector does not try to prove that the assertion actually guards the right value. This keeps the precision/recall trade-off tilted toward few false positives.

Limitations

  • Validation expressed through component modifiers (for example an Ownable guard) rather than inline assert is not recognised and produces a false positive.
  • The presence of any assertion suppresses the finding, so a function that asserts an unrelated property is treated as validated.
  • The detector reports the absence of validation, not its correctness; an asserted function with a wrong or weak check is outside its scope.

References

Sample Sigvex Output

{
  "detector": "cairo-external-without-validation",
  "severity": "medium",
  "confidence": 0.65,
  "title": "External mutating function `set_owner` validates no inputs at line 2",
  "template": "set_owner",
  "signal": "set_owner",
  "line": 2,
  "description": "External function `set_owner` accepts 1 parameter(s) and mutates state (storage write or external call) but contains no `assert` statements. Any caller can drive the function with arbitrary inputs; missing input validation frequently enables access-control bypasses, ill-formed state, or privilege escalation.",
  "recommendation": "Add `assert` guards on caller identity (e.g. `assert(get_caller_address() == owner)`) and on input shape (ranges, non-zero addresses, amount bounds) before any state change."
}