Safeguard
Developer Tools•

Property-based testing for input validation: pick invariants

How to choose properties for security-sensitive code, such as round trips and never-crash rules, so a generator finds the inputs you did not think of.

Safeguard Security Team
3 min read

Property-based testing means you state a rule that must always hold, and a library generates many inputs to try to break it. For security code, the useful rules are round trips, idempotence, and "never crashes or escapes". The hard part is picking the rule, not writing the test.

What a property is

A normal unit test says: for this input, expect this output. A property says: for any input of this shape, this statement is true. In Python's Hypothesis, you pair a decorator called @given with a strategy that describes the inputs, and the library generates values for you. The introduction in its documentation shows this with integers and lists before moving on to harder cases.

The payoff is that the generator tries values that a human skips: empty strings, huge numbers, odd Unicode, nested structures.

Four properties that suit security code

Pick from this short list before inventing anything.

  • Round trip. Decoding an encoded value returns the original. Useful for token formats, serializers and escaping functions.
  • Never crashes. Any input to a parser either returns a result or raises the documented error type, and nothing else.
  • Idempotence. Sanitizing twice gives the same result as sanitizing once. A mismatch hints that the first pass leaves something the second pass changes.
  • Output safety. For any input, the escaped output contains no raw delimiter characters.

Example: a path joiner

from hypothesis import given, strategies as st
from pathlib import PurePosixPath
from safe_join import safe_join

BASE = "/srv/uploads"

@given(st.text())
def test_never_escapes_base(user_part):
    try:
        result = safe_join(BASE, user_part)
    except ValueError:
        return
    assert PurePosixPath(result).is_relative_to(BASE)

The test does not list traversal strings. It says only that any accepted result stays under the base directory. The generator does the searching.

Keep it deterministic when it fails

A property test that finds a bug must be repeatable, or the failure looks flaky. Hypothesis documents how to replay failed tests, and a good practice is to commit the failing case as an ordinary unit test once you have fixed the bug. That turns a random find into a permanent regression check.

Limits worth knowing

Generators are random, so they can miss inputs that need very specific structure, such as a valid signature. For those, build the structure in the strategy, or move to coverage guided fuzzing, which uses feedback from the code to reach deeper branches.

Properties also need an oracle. If you cannot say what correct means, do not force one. A never-crash property is always available as a fallback.

Quick check

Do I replace example tests with property tests?

No. Keep examples for documented behavior and known attacks. Add properties for broad rules. They answer different questions.

How many inputs does a property test run?

It depends on the library and your settings. Check the documentation of the tool you use rather than assuming a number, and raise the count for a nightly run if the default is low.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.