contracts.rules_security_stack

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

Security-stack contract check — secure-by-default as a contract (#182).

This category is the canonical owner of the mutating routedefinition referenced by the forms/auth epics. Other rules that need "what counts as…

Security-stack contract check — secure-by-default as a contract (#182).

This category is the canonical owner of the mutating route definition referenced by the forms/auth epics. Other rules that need "what counts as a mutating route" importMUTATING_METHODS / is_mutating_routefrom here.

A route is mutating when either is true:

  1. It accepts a mutating HTTP method (POST/PUT/PATCH/DELETE), or
  2. It is a filesystem page that ships_actions.pyform actions (route.actionsis non-empty). This is Chirp's canonical form-action pattern: thepage.py may declare only get(), yet the page mutates state via POST-to-self dispatched on the_actionform field. Such a page is method-GET in the router but is unmistakably a mutating surface, so it must clear the same CSRF/Session bar as a POST route.

Category:

  • security_stack: a mutating route exists but the security stack is not fully wired.

Severity matrix (env-aware, mirroringrules_safety/rules_deploy):

  • MissingCSRFMiddleware or SessionMiddlewareon an app with any mutating route →ERROR in production, WARNINGin staging, and silent in development (the default) so dev apps and shipped examples stay clean.
  • MissingSecurityHeadersMiddlewareWARNINGunconditionally (whenever any mutating route exists), independent of env. This is the agreed decision: CSRF/Session and SecurityHeaders are two distinct severity tracks.

No middleware is force-injected intoApp(). The lever is this contract plus scaffold defaults (#183), per the explicit-over-magic convention.

Middleware presence is detected by class name (type(mw).__name__), not isinstance. This matches the established pattern in rules_safety.check_csrf_session_orderand rules_csrf_forms._csrf_middleware_active: it avoids importing middleware classes into the contracts layer (keeping the dependency direction clean). The trade-off is that a user subclass is only recognised when it keeps the same class name.

contracts.rules_security_stack

Name Type Default Description
type
qualified_name
element_type
description
source_file
line_number
is_autodoc
autodoc_element
_autodoc_template
_autodoc_url_path
_autodoc_page_type
title
doc_content_hash

Symbols on this page

is_mutating_route
function
def is_mutating_route(route: Any) -> bool

Return True whenrouteis a mutating surface.

Canonical predicate for the "mutating route" definition. A route is mutating when either:

  • it accepts a mutating HTTP method —route.methodsintersects MUTATING_METHODS; or
  • it is a filesystem page carrying_actions.pyform actions — route.actionsis non-empty. These pages mutate state via POST-to-self on the_action form field even when page.pydeclares only get(), so they must be treated as mutating.

RuntimeRoute objects expose methods only; discovered PageRoute objects expose bothmethods and actions. getattrdefaults keep the predicate total for either shape.

Parameters

Name Type Default Description
route Any
check_security_stack
function
def check_security_stack(router: Router, config: Any, middleware_list: list[Any], discovered_routes: list[Any] | None = None) -> list[ContractIssue]

Flag mutating routes that are not protected by the security stack.

A mutating route needs CSRF + session protection. A route is mutating when it accepts a mutating HTTP method (POST/PUT/PATCH/DELETE) or is a filesystem page carrying_actions.pyform actions (POST-to-self on the _action field; see is_mutating_route()). When either CSRFMiddleware or SessionMiddlewareis missing, this ERRORs in production and WARNs in staging (silent in development). Missing SecurityHeadersMiddlewarealways WARNs. No issue is emitted for an app with no mutating routes.

discovered_routes carries the filesystem PageRouteobjects (which exposeactions); runtime router.routes expose methodsonly. Both are scanned so a GET-only page backed by form actions is still flagged.

Note: referenced (transport) routes are included — a mutating SSE/API endpoint still needs CSRF/session protection, unlike the no-JS floor which excludes them. This is intentional and flagged for steward sign-off.

Parameters

Name Type Default Description
router Router
config Any
middleware_list list[Any]
discovered_routes list[Any] | None None

View source · /home/runner/work/chirp/chirp/site/../src/chirp/contracts/rules_security_stack.py:1