def cancel_order(order_id, orders):
    """Cancel an order on behalf of the person who placed it."""
    order = orders[order_id]
    order.status = "cancelled"
    return order

This function takes an order ID and nothing else. It has no way to know who the caller is, so it cannot check that the caller owns the order. The code is deliberately broken; it comes from Perch’s example order service.

Require ownership before cancellation

Save this rule in perch.yaml:

- name: cancellation-requires-owner
  where: auth.py
  each: method
  ensure: >-
    If this method cancels an order, it checks that the
    authenticated caller owns that order before changing it.
    Possession of an order ID alone is insufficient. A method
    that does not cancel orders satisfies this rule.

After setting up the example, check the function:

perch check auth.py::cancel_order --rules cancellation-requires-owner

The rule states the access policy directly. In a real application, authorization often happens in the caller before this function runs, so check the caller too.

Reject a different owner

One way to fix it, shown here as an illustrative example, is to take an actor_id from the trusted authentication layer and check it before changing the order:

def cancel_order(order_id, orders, actor_id):
    order = orders[order_id]
    if order.owner_id != actor_id:
        raise PermissionError("Order belongs to another user")
    order.status = "cancelled"
    return order

Never take actor_id from a request field the user controls. Test the fix at the HTTP boundary: cancel the order as its owner, as a different user, and without signing in. Only the owner’s request should succeed, and the other two must leave the order unchanged.

Rerun the rule after the edit. A repository scan reads committed code, so commit the change before scanning.

Scan for other security issues

perch scan --filter type=security

The built-in checks ask about authorization, injection, weak crypto, and other security classes. In our recorded combined scan, Perch flagged this method at 78% for weak crypto:

def token_for(user_id):
    """A stable token identifying a user to the order service."""
    return hashlib.md5(str(user_id).encode()).hexdigest()

Hashing a predictable user ID says nothing about who the caller is. That scan did not report the missing owner check in cancel_order. Ownership is specific to this application, which is why it needs the custom rule above.

For access policies in a web framework, see the FastAPI and NestJS examples.