Version: 2026-08-11 Audience: Core maintainers, security engineers, application developers Purpose: Document Shield (security headers), CORS, CSRF protection, and rate limiting. The four security middleware components
Overview
Section titled “Overview”Sillo’s security layer consists of four independent middleware components, each handling a specific aspect of HTTP security:
flowchart TD
REQ[Incoming Request] --> SHIELD[Shield]
SHIELD --> CORS[CORSMiddleware]
CORS --> CSRF[CSRFMiddleware]
CSRF --> RL[RateLimitMiddleware]
RL --> HANDLER[Route Handler]
HANDLER --> RESP[Response]
RL -->|429| RESP
CSRF -->|403| RESP
CORS -->|preflight| RESP
SHIELD -->|SSL redirect| RESP
| Middleware | Purpose | Default State |
|---|---|---|
Shield | HTTP security headers (CSP, HSTS, XSS, etc.) | Enabled (headers on every response) |
CORSMiddleware | Cross-origin request handling | Disabled (must be explicitly configured) |
CSRFMiddleware | CSRF token validation | Disabled (enabled=False by default) |
RateLimitMiddleware | Request rate limiting | Disabled (must be explicitly added) |
Security Middleware Architecture
Section titled “Security Middleware Architecture”None of the four extends BaseMiddleware. Each is plain raw ASGI —
__init__(self, ...) setting self.app = None, and
async def __call__(self, scope, receive, send) — registered the way it
always has been, as an already-configured instance
(app.use(Shield(...))), which SilloApp.use() recognises isn’t a bare class
and binds next_app onto directly. See the middleware architecture reference
(§19) for the shared pieces this makes possible without a shared base class:
HttpContext built directly inside __call__ rather than through the
dispatch bridge, and ResponseHeaders for editing a response’s headers in
place.
None of them shares a dispatch/call_next hook, either — each names its
own methods for what it actually does:
| Middleware | Methods |
|---|---|
Shield | apply_security_headers(headers) |
CORSMiddleware | check_request(ctx), apply_cors_headers(origin, headers) |
CSRFMiddleware | validate(ctx), set_token_cookie(ctx, headers) |
RateLimitMiddleware | check(ctx), set_limit_headers(headers, result) |
The “before” half (reading the request, deciding whether to short-circuit)
runs in __call__ itself before the downstream app; the “after” half
(editing response headers) runs from a closure __call__ wraps send in,
called when the downstream app’s http.response.start message passes
through.
Shield: HTTP Security Headers
Section titled “Shield: HTTP Security Headers”File: core/sillo/security/shield.py
Overview
Section titled “Overview”Shield injects comprehensive HTTP security headers into every response. It also handles SSL redirect. A backward-compatibility alias exists at core/sillo/middleware/security.py:
SecurityMiddleware = ShieldConstructor Parameters
Section titled “Constructor Parameters”class Shield: def __init__( self, # Content Security Policy csp_enabled: bool = True, csp_policy: dict[str, str | list[str]] | None = None, csp_report_only: bool = False, # HSTS hsts_enabled: bool = True, hsts_max_age: int = 31536000, hsts_include_subdomains: bool = True, hsts_preload: bool = False, # XSS Protection xss_protection: bool = True, xss_mode: str = "block", # Frame Options frame_options: str = "DENY", frame_options_allow_from: str | None = None, # Content Type Options content_type_options: bool = True, # Referrer Policy referrer_policy: str = "strict-origin-when-cross-origin", # Permissions Policy permissions_policy: dict[str, str | list[str]] | None = None, # SSL/HTTPS ssl_redirect: bool = False, ssl_host: str | None = None, ssl_permanent: bool = True, # Cache Control cache_control: str = "no-store, no-cache, must-revalidate, proxy-revalidate", # Cross-Origin Options cross_origin_opener_policy: str = "same-origin", cross_origin_embedder_policy: str = "require-corp", cross_origin_resource_policy: str = "same-origin", # Expect-CT expect_ct: bool = False, expect_ct_max_age: int = 86400, expect_ct_enforce: bool = False, expect_ct_report_uri: str | None = None, # Trusted Types trusted_types: bool = False, trusted_types_policies: list[str] | None = None, # Server hide_server: bool = True, server_header: str | None = None, ... )Headers Emitted
Section titled “Headers Emitted”| Header | Default Value | Purpose |
|---|---|---|
Content-Security-Policy | default-src 'self'; script-src 'self'; ... | Prevents XSS, data injection |
Strict-Transport-Security | max-age=31536000; includeSubDomains | Forces HTTPS |
X-XSS-Protection | 1; mode=block | Legacy XSS filter |
X-Frame-Options | DENY | Prevents clickjacking |
X-Content-Type-Options | nosniff | Prevents MIME sniffing |
Referrer-Policy | strict-origin-when-cross-origin | Controls referrer information |
Permissions-Policy | (empty by default) | Controls browser feature access |
Cache-Control | no-store, no-cache, must-revalidate, proxy-revalidate | Prevents caching sensitive data |
Cross-Origin-Opener-Policy | same-origin | Isolates browsing context |
Cross-Origin-Embedder-Policy | require-corp | Controls cross-origin embedding |
Cross-Origin-Resource-Policy | same-origin | Controls cross-origin reads |
X-DNS-Prefetch-Control | off | Controls DNS prefetching |
X-Download-Options | noopen | Prevents IE file download execution |
Default CSP Policy
Section titled “Default CSP Policy”{ "default-src": ["'self'"], "script-src": ["'self'"], "style-src": ["'self'"], "img-src": ["'self'"], "connect-src": ["'self'"], "font-src": ["'self'"], "object-src": ["'none'"], "media-src": ["'self'"], "frame-src": ["'none'"], "base-uri": ["'self'"], "form-action": ["'self'"],}SSL Redirect
Section titled “SSL Redirect”When ssl_redirect=True, any HTTP request is redirected to HTTPS:
from sillo import redirect
# Inside __call__, before the downstream app runsif self.ssl_redirect and ctx.url.scheme != "https": redirect_url = f"https://{self.ssl_host or ctx.url.hostname}{ctx.url.path}" response = redirect(url=redirect_url, status_code=301 if self.ssl_permanent else 302) await response(scope, receive, send) returnssl_host: override the hostname (e.g. for load balancers)ssl_permanent:Truefor 301,Falsefor 302
CSP Header Building
Section titled “CSP Header Building”def _build_csp_header(self) -> str: policies = [] for directive, sources in self.csp_policy.items(): if isinstance(sources, str): sources = [sources] policies.append(f"{directive} {' '.join(sources)}") return "; ".join(policies)Trusted Types
Section titled “Trusted Types”When trusted_types=True, the CSP header is extended with:
require-trusted-types-for 'script'; trusted-types <policies>Server Header
Section titled “Server Header”hide_server=True(default): removes theServerheaderhide_server=False, server_header="MyApp/1.0": sets a custom server header
How the headers actually get applied
Section titled “How the headers actually get applied”apply_security_headers(headers) — the ResponseHeaders editor for the
http.response.start message — builds a dict from every header the response
already has, adds Shield’s own into it, and writes the result back with
headers.set_headers(computed, override_all=True). override_all=True
matters: set_headers() defaults to appending each entry rather than
replacing, and computed already contains the response’s own pre-existing
headers alongside Shield’s additions, so the default would append a second,
duplicate copy of every header Shield never meant to touch —
Content-Type and Content-Length included. This was a real bug in the
dispatch-based version of Shield, fixed as part of moving it to raw ASGI.
CORSMiddleware: Cross-Origin Resource Sharing
Section titled “CORSMiddleware: Cross-Origin Resource Sharing”Files:
core/sillo/security/cors/config.py:CorsConfigcore/sillo/security/cors/_middleware.py:CORSMiddleware
CorsConfig
Section titled “CorsConfig”class CorsConfig: def __init__( self, allow_origins: list[str] | None = None, blacklist_origins: list[str] | None = None, allow_methods: list[str] | None = None, blacklist_headers: list[str] | None = None, allow_headers: list[str] | None = None, allow_credentials: bool = True, allow_origin_regex: str | None = None, expose_headers: list[str] | None = None, max_age: int = 600, strict_origin_checking: bool = False, dynamic_origin_validator: Callable[[str | None], bool] | None = None, debug: bool = False, custom_error_status: int = 400, custom_error_messages: dict[str, str] | None = None, )| Parameter | Default | Purpose |
|---|---|---|
allow_origins | [] | List of allowed origins. ["*"] allows all. |
blacklist_origins | [] | Origins always denied (checked before allow) |
allow_methods | GET, POST, PUT, DELETE, PATCH, OPTIONS | Allowed HTTP methods |
blacklist_headers | [] | Headers always denied |
allow_headers | [] | Additional allowed headers (safelisted headers always included) |
allow_credentials | True | Whether to include Access-Control-Allow-Credentials |
allow_origin_regex | None | Regex pattern for allowed origins |
expose_headers | [] | Headers exposed to the browser |
max_age | 600 | Preflight cache duration (seconds) |
strict_origin_checking | False | Reject requests without Origin header |
dynamic_origin_validator | None | Callable for runtime origin validation |
Origin Validation
Section titled “Origin Validation”def is_allowed_origin(self, origin: str | None) -> bool: if origin in self.blacklist_origins: return False if "*" in self.allow_origins: return True if self.allow_origin_regex and self.allow_origin_regex.fullmatch(origin): return True if self.dynamic_origin_validator and callable(self.dynamic_origin_validator): return self.dynamic_origin_validator(origin) return origin in self.allow_originsValidation order:
- Blacklist check (always first)
- Wildcard
"*"check - Regex pattern match
- Dynamic validator callback
- Exact match in
allow_origins
Preflight Handling
Section titled “Preflight Handling”sequenceDiagram
participant Browser
participant CORS as CORSMiddleware
Browser->>CORS: OPTIONS /api/data
Note right of Browser: Origin: https://app.example.com
Note right of Browser: Access-Control-Request-Method: POST
Note right of Browser: Access-Control-Request-Headers: Content-Type
CORS->>CORS: is_allowed_origin(origin)
CORS->>CORS: is_allowed_method(requested_method)
CORS->>CORS: Check requested headers
alt All checks pass
CORS-->>Browser: 201 OK
Note left of CORS: Access-Control-Allow-Origin: https://app.example.com
Note left of CORS: Access-Control-Allow-Methods: POST
Note left of CORS: Access-Control-Allow-Headers: content-type
Note left of CORS: Access-Control-Max-Age: 600
else Check fails
CORS-->>Browser: 400 CORS request denied.
end
Simple Requests
Section titled “Simple Requests”For non-preflight requests, the middleware:
- Runs the downstream app (
check_requestfound nothing to reject or answer) - Once its response starts,
apply_cors_headerssetsAccess-Control-Allow-Originif the origin is allowed - Sets
Access-Control-Allow-Credentialsif configured - Sets
Access-Control-Expose-Headersif configured
Safelisted Headers
Section titled “Safelisted Headers”The following headers are always allowed (per the CORS spec):
SAFELISTED_HEADERS = {"accept", "accept-language", "content-language", "content-type"}CSRFMiddleware: Cross-Site Request Forgery Protection
Section titled “CSRFMiddleware: Cross-Site Request Forgery Protection”Files:
core/sillo/security/csrf/config.py:CSRFConfigcore/sillo/security/csrf/_middleware.py:CSRFMiddleware
CSRFConfig
Section titled “CSRFConfig”class CSRFConfig: def __init__( self, enabled: bool = False, # DISABLED by default required_urls: list[str] | None = None, exempt_urls: list[str] | None = None, sensitive_cookies: list[str] | None = None, safe_methods: list[str] | None = None, cookie_name: str = "csrftoken", cookie_path: str = "/", cookie_domain: str | None = None, cookie_secure: bool = False, cookie_httponly: bool = True, cookie_samesite: Literal["lax", "none", "strict"] = "lax", header_name: str = "X-CSRFToken", secret_key: str | None = None, )| Parameter | Default | Purpose |
|---|---|---|
enabled | False | CSRF protection is disabled by default |
required_urls | ["*"] | URL patterns requiring CSRF validation |
exempt_urls | [] | URL patterns exempt from CSRF validation |
sensitive_cookies | [] | Cookies that trigger CSRF validation on exempt URLs |
safe_methods | GET, HEAD, OPTIONS, TRACE | HTTP methods that skip CSRF validation |
cookie_name | "csrftoken" | Name of the CSRF cookie |
header_name | "X-CSRFToken" | Name of the CSRF header |
secret_key | None | Secret for signing tokens (required for operation) |
Token Generation
Section titled “Token Generation”The middleware uses an internal URLSafeSerializer to sign tokens:
def _generate_csrf_token(self) -> str: return self.serializer.dumps(secrets.token_urlsafe(32))A random token is generated with secrets.token_urlsafe(32), then signed with HMAC-SHA256. The signed token is stored in a cookie and must be submitted back in the X-CSRFToken header.
Double-Submit Cookie Pattern
Section titled “Double-Submit Cookie Pattern”sequenceDiagram
participant Client
participant MW as CSRFMiddleware
Note over Client,MW: GET /form (safe method)
Client->>MW: GET /form
MW->>MW: Generate CSRF token
MW-->>Client: Set-Cookie: csrftoken=<signed_token>
Note over Client,MW: POST /form (unsafe method)
Client->>MW: POST /form
Note right of Client: Cookie: csrftoken=<signed_token>
Note right of Client: Header: X-CSRFToken=<signed_token>
MW->>MW: Read cookie token
MW->>MW: Read header token
MW->>MW: _csrf_tokens_match(cookie, header)
MW->>MW: Verify signature + constant-time compare
MW-->>Client: 200 OK (or 403 if mismatch)
Token Verification
Section titled “Token Verification”def _csrf_tokens_match(self, token1, token2) -> bool: try: decoded1 = self.serializer.loads(token1) decoded2 = self.serializer.loads(token2) return secrets.compare_digest(decoded1, decoded2) except BadSignature: return FalseBoth tokens are decoded (signature verified) and then compared with constant-time comparison.
URL Matching
Section titled “URL Matching”def _url_is_required(self, url: str) -> bool: if not self.required_urls: return False if "*" in self.required_urls: return True for required_url in self.required_urls: match = re.match(required_url, url) if match and match.group() == url: return True return FalseURL patterns are matched as regexes with exact-match semantics (the match must cover the entire URL).
Request Processing Flow
Section titled “Request Processing Flow”flowchart TD
A[Request received] --> B{CSRF enabled?}
B -->|no| C[Continue to handler]
B -->|yes| D{Safe method?}
D -->|yes| C
D -->|no| E{URL required?}
E -->|no| C
E -->|yes| F[Read cookie token]
F --> G{Cookie present?}
G -->|no| H[403 CSRF token missing from cookies]
G -->|yes| I[Read header token]
I --> J{Header present?}
J -->|no| K[403 CSRF token missing from headers]
J -->|yes| L{Tokens match?}
L -->|no| M[403 CSRF token incorrect]
L -->|yes| C
Rate Limiting
Section titled “Rate Limiting”Files:
core/sillo/security/ratelimit/config.py:RateLimitConfigcore/sillo/security/ratelimit/_middleware.py:RateLimitMiddlewarecore/sillo/security/ratelimit/__init__.py:RateLimitconvenience classcore/sillo/security/ratelimit/strategies/base.py:RateLimitStrategy(abstract)core/sillo/security/ratelimit/strategies/token_bucket.py:TokenBucketStrategycore/sillo/security/ratelimit/strategies/fixed_window.py:FixedWindowStrategycore/sillo/security/ratelimit/strategies/sliding_window.py:SlidingWindowStrategycore/sillo/security/ratelimit/backends/base.py:RateLimitBackend(abstract),RateLimitResultcore/sillo/security/ratelimit/backends/memory.py:InMemoryBackendcore/sillo/security/ratelimit/backends/redis.py:RedisBackendcore/sillo/security/ratelimit/backends/record.py:RecordBackendcore/sillo/security/ratelimit/models.py:RateLimitCounter
RateLimitConfig
Section titled “RateLimitConfig”from sillo import HttpContext
class RateLimitConfig: def __init__( self, limit: int = 60, window: int = 60, strategy: str | Any = "token", backend: str | Any = "memory", key_func: Callable[[HttpContext], str | None] | None = None, namespace: str = "sillo_rl", cost: int = 1, include_headers: bool = True, fail_open: bool = True, on_exceed: str | Callable = "deny", )| Parameter | Default | Purpose |
|---|---|---|
limit | 60 | Maximum requests per window |
window | 60 | Time window in seconds |
strategy | "token" | Algorithm: "token", "fixed", "sliding", or a strategy instance |
backend | "memory" | Storage: "memory", "redis", "record", or a backend instance |
key_func | Client IP | Function to extract rate-limit key from request |
namespace | "sillo_rl" | Prefix for backend keys |
cost | 1 | Tokens consumed per request |
include_headers | True | Emit X-RateLimit-* headers |
fail_open | True | Allow requests if backend fails |
on_exceed | "deny" | "deny" (returns 429) or a callable |
Strategies
Section titled “Strategies”All strategies implement RateLimitStrategy.hit():
class RateLimitStrategy(ABC): @abstractmethod async def hit(self, backend, key, limit, window, cost=1, now=None) -> RateLimitResult: ...Token Bucket (Default)
Section titled “Token Bucket (Default)”File: core/sillo/security/ratelimit/strategies/token_bucket.py
Maintains a bucket of limit tokens refilled at limit / window tokens per second. Each request consumes cost tokens. Allows short bursts up to limit then smoothly throttles.
refill_rate = limit / window # tokens per secondtokens = min(limit, state["tokens"] + elapsed * refill_rate)if tokens < cost: # Denied — calculate retry_after ...tokens -= costCharacteristics:
- Smooth rate limiting with burst support
- Best client experience
- State:
{"tokens": float, "last": float}
Fixed Window
Section titled “Fixed Window”File: core/sillo/security/ratelimit/strategies/fixed_window.py
Counts requests within a fixed time window starting at the first hit. Resets completely when the window elapses.
window_start = int(now // window) * windowif state is None or state.get("window_start") != window_start: state = {"window_start": window_start, "count": 0}Characteristics:
- Simplest algorithm
- Allows bursts at window boundaries (the classic “double count” at the edge)
- Cheap and predictable
- State:
{"window_start": float, "count": int}
Sliding Window
Section titled “Sliding Window”File: core/sillo/security/ratelimit/strategies/sliding_window.py
Tracks individual request timestamps and only counts those within the last window seconds. Eliminates the boundary double-count problem.
cutoff = now - windowhits = [t for t in hits if t > cutoff]if len(hits) + cost > limit: # Denied ...Characteristics:
- Most accurate: no boundary issues
- State grows with request volume (pruned each hit)
- State:
{"hits": list[float]}
Backends
Section titled “Backends”All backends implement RateLimitBackend:
class RateLimitBackend: async def fetch_state(self, key: str) -> dict | None: ... async def save_state(self, key: str, state: dict, ttl: int) -> None: ... async def clear(self) -> None: ...InMemoryBackend
Section titled “InMemoryBackend”File: core/sillo/security/ratelimit/backends/memory.py
Process-local storage using a dict with asyncio.Lock for coroutine safety.
class InMemoryBackend(RateLimitBackend): def __init__(self): self._store: dict[str, tuple[dict, float]] = {} self._lock = asyncio.Lock()- Suitable for single-instance deployments and tests
- State expired lazily by timestamp (no background cleanup)
- Lost on process restart
RedisBackend
Section titled “RedisBackend”File: core/sillo/security/ratelimit/backends/redis.py
Redis-backed shared storage using JSON serialization and a Lua script for atomic read-modify-write.
class RedisBackend(RateLimitBackend): def __init__(self, url="redis://localhost:6379/0", prefix="sillo:ratelimit:", **kwargs): import redis.asyncio as aioredis self._client = aioredis.from_url(url, **kwargs) self._script = self._client.register_script(_LUA_SET)- Recommended for multi-instance deployments
- Atomic operations via Lua script
- Requires
redispackage
RecordBackend
Section titled “RecordBackend”File: core/sillo/security/ratelimit/backends/record.py
Stores state in the application database via the RateLimitCounter Tortoise model.
- Uses
sillo.recordORM - No external dependencies beyond the database
- Single-instance-level atomicity
RateLimitResult
Section titled “RateLimitResult”File: core/sillo/security/ratelimit/backends/base.py
@dataclassclass RateLimitResult: allowed: bool # Whether the ctx is permitted limit: int # The configured maximum remaining: int # Requests left in window reset_at: float # Unix timestamp when window resets retry_after: int # Seconds to wait before retrying (0 when allowed)RateLimitMiddleware
Section titled “RateLimitMiddleware”File: core/sillo/security/ratelimit/_middleware.py
class RateLimitMiddleware: def __init__(self, config=None, **kwargs): ...
async def __call__(self, scope, receive, send) -> None: ...Plain raw ASGI, not a BaseMiddleware subclass. __call__ calls check
before running the downstream app, and wraps send to call
set_limit_headers on the http.response.start message:
from sillo import HttpContext
async def check(self, ctx: HttpContext): key = self.config._key_func(ctx) if key is None: return None # No key → skip limiting
full_key = f"{self.config.namespace}:{key}" try: return await self._strategy.hit( self._backend, full_key, self.config.limit, self.config.window, cost=self.config.cost, ) except Exception: if not self.config.fail_open: raise return None # Backend failed, fail_open=Trueasync def __call__(self, scope, receive, send) -> None: app = self._inner() if scope["type"] != "http": await app(scope, receive, send) return
ctx = HttpContext(scope, receive) result = await self.check(ctx)
if result is not None and not result.allowed: response = self._deny(ctx, result) # 429, downstream app never runs await response(scope, receive, send) return
async def send_with_limit_headers(message): if message["type"] == "http.response.start": self.set_limit_headers(ResponseHeaders(message), result) await send(message)
await app(scope, receive, send_with_limit_headers)def set_limit_headers(self, headers, result) -> None: if result is None or not self.config.include_headers: return headers.set_header("X-RateLimit-Limit", str(result.limit), override=True) headers.set_header("X-RateLimit-Remaining", str(result.remaining), override=True) headers.set_header("X-RateLimit-Reset", str(int(result.reset_at)), override=True)The headers go on only when the request was allowed through. A denied request
gets its counts from _deny, which builds the 429 with them already set.
Notice result is a local variable threaded through the send_with_limit_headers
closure, not stored on self. It used to be (self._last_result = result)
in the dispatch-based version — a real bug, since one middleware instance
serves every concurrent request the application handles: a second request’s
result landing on self between it being set and _set_limit_headers
reading it back would have stamped the wrong numbers on the first request’s
response. Fixed as part of moving this to raw ASGI, where each request’s
result naturally lives in its own __call__ invocation instead.
429 response:
from sillo import HttpContext, json
def _deny(self, ctx: HttpContext, result): retry_after = max(int(result.retry_after), 1) return json( { "error": "rate_limit_exceeded", "message": "Too many requests. Slow down and retry later.", "retry_after": retry_after, }, status_code=429, headers={ "X-RateLimit-Limit": str(result.limit), "X-RateLimit-Remaining": "0", "X-RateLimit-Reset": str(int(result.reset_at)), "Retry-After": str(retry_after), }, )RateLimit Convenience Class
Section titled “RateLimit Convenience Class”File: core/sillo/security/ratelimit/__init__.py
class RateLimit(RateLimitMiddleware): def __init__(self, limit=60, window=60, strategy="token", backend="memory", key_func=None, namespace="sillo_rl", cost=1, include_headers=True, fail_open=True, on_exceed="deny", **kwargs): config = RateLimitConfig(limit=limit, window=window, ...) super().__init__(config=config, **kwargs)Usage:
app.use(RateLimit(limit=100, window=60, backend="redis"))Rate Limit Architecture
Section titled “Rate Limit Architecture”flowchart TD
subgraph "Middleware"
MW[RateLimitMiddleware]
end
subgraph "Strategies"
TB[TokenBucketStrategy]
FW[FixedWindowStrategy]
SW[SlidingWindowStrategy]
end
subgraph "Backends"
MEM[InMemoryBackend]
REDIS[RedisBackend]
REC[RecordBackend]
end
MW --> TB
MW --> FW
MW --> SW
TB --> MEM
TB --> REDIS
TB --> REC
FW --> MEM
FW --> REDIS
FW --> REC
SW --> MEM
SW --> REDIS
SW --> REC
Middleware Composition
Section titled “Middleware Composition”Recommended Order
Section titled “Recommended Order”from sillo.security import Shield, CORSMiddleware, CSRFMiddlewarefrom sillo.security.ratelimit import RateLimit
app = SilloApp()
# 1. Shield — adds security headers to every responseapp.use(Shield())
# 2. CORS — handles cross-origin requestsapp.use(CORSMiddleware(CorsConfig(allow_origins=["https://app.example.com"])))
# 3. CSRF — validates tokens on unsafe methodsapp.use(CSRFMiddleware(CSRFConfig(enabled=True, secret_key="...")))
# 4. Rate limiting — protects against abuseapp.use(RateLimit(limit=100, window=60, backend="redis"))Why this order?
- Shield runs first to ensure headers are on every response (including error responses)
- CORS runs before CSRF because preflight requests (OPTIONS) should bypass CSRF
- Rate limiting runs last to count all requests, including those rejected by CSRF
Interaction with Authentication
Section titled “Interaction with Authentication”The security middleware runs independently of authentication. A typical full stack:
app.use(Shield())app.use(CORSMiddleware(cors_config))app.use(SessionMiddleware(config=session_config))app.use(AuthenticationMiddleware(user_model=User, backend=[JWTAuthBackend(...)]))app.use(RateLimit(limit=100, window=60))Source Map
Section titled “Source Map”| Component | File | Lines |
|---|---|---|
Shield | core/sillo/security/shield.py | 18-245 |
SecurityMiddleware alias | core/sillo/middleware/security.py | 5 |
CorsConfig | core/sillo/security/cors/config.py | 5-125 |
CORSMiddleware | core/sillo/security/cors/_middleware.py | 20-249 |
CSRFConfig | core/sillo/security/csrf/config.py | 5-117 |
CSRFMiddleware | core/sillo/security/csrf/_middleware.py | 14-162 |
RateLimitConfig | core/sillo/security/ratelimit/config.py | 13-71 |
RateLimitMiddleware | core/sillo/security/ratelimit/_middleware.py | 26-100 |
RateLimit | core/sillo/security/ratelimit/__init__.py | 51-87 |
RateLimitStrategy | core/sillo/security/ratelimit/strategies/base.py | 19-64 |
TokenBucketStrategy | core/sillo/security/ratelimit/strategies/token_bucket.py | 19-57 |
FixedWindowStrategy | core/sillo/security/ratelimit/strategies/fixed_window.py | 18-56 |
SlidingWindowStrategy | core/sillo/security/ratelimit/strategies/sliding_window.py | 19-55 |
RateLimitResult | core/sillo/security/ratelimit/backends/base.py | 16-33 |
RateLimitBackend | core/sillo/security/ratelimit/backends/base.py | 36-49 |
InMemoryBackend | core/sillo/security/ratelimit/backends/memory.py | 17-45 |
RedisBackend | core/sillo/security/ratelimit/backends/redis.py | 27-71 |
RecordBackend | core/sillo/security/ratelimit/backends/record.py | 16-29 |
RateLimitCounter model | core/sillo/security/ratelimit/models.py | 19 |