Startup-time security posture and baseline management plugin for FastAPI applications.
Project description
FastAPI Safeguard
A zero-runtime-overhead security linting and hardening plugin for FastAPI applications.
Catches security issues at startup—before serving traffic. Prevents accidental exposure of unsecured endpoints through baseline lock files and explicit intent markers.
"Errors should never pass silently." — Unless explicitly accepted and version‑locked in your baseline.
✨ Highlights
- 🔒 Baseline Lock File — Like
package-lock.jsonbut for security findings. Accept current tech debt; fail only on new regressions. - 🚨 Fail Fast — Detects missing auth, exposed sensitive fields, CORS misconfigs, debug mode, and more at startup.
- 🎯 Explicit Intent —
@open_routefor public endpoints,@disable_security_checksfor exceptions. No accidental exposures. - 📊 OWASP API Top 10 — Covers API2, API3, API4, API5, API6, API8 with 9 high-value core checks.
- ⚡ Zero Runtime Overhead — All checks run once at startup, not per-request.
- 🔧 Extensible — Write custom checks, configure per-org rules. 8+ optional checks available.
🚀 Quick Start
pip install fastapi-safeguard
from fastapi import FastAPI, Depends
from fastapi.security import OAuth2PasswordBearer
from fastapi_safeguard import FastAPISafeguard, open_route
app = FastAPI(lifespan=FastAPISafeguard.recommended().lifespan())
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@open_route # Explicitly marked as public - no warning
@app.get("/health")
async def health():
return {"status": "healthy"}
@app.get("/legacy-endpoint") # Forgot to add auth - will be flagged!
async def legacy():
return {"data": "visible to all"}
@app.get("/protected")
async def protected(token: str = Depends(oauth2_scheme)):
return {"data": "secret"}
First run - Accept current state and create baseline:
SECURITY_BASELINE_UPDATE=1 uvicorn main:app --reload
This creates security_baseline.json:
{
"schema_version": 1,
"generated_at": "2025-01-15T10:30:00Z",
"accepted_findings": [
"GET /legacy-endpoint has no accepted security dependency"
],
"checks_count": 9
}
Subsequent runs - Fail only on new findings:
uvicorn main:app --reload
✅ Startup succeeds if findings match baseline ❌ Startup fails if new security issues detected
Table of Contents
- Highlights
- Quick Start
- Why
- Key Features
- Installation & Usage
- Baseline (Lock) File Workflow
- Decorators
- Configuration & Extensibility
- Writing a Custom Check
- Startup Output Examples
- Security Checks Reference
- OWASP API Top 10 Coverage
- CI/CD Integration
- Environment Variables
- FAQ
- Roadmap
- Contributing
- Security Policy
- License
Why
Securing an API is more than adding a single dependency or middleware. Repeated patterns cause recurring mistakes:
- Accidentally shipping a mutating endpoint with no auth.
- Returning whole ORM objects / dict bodies without explicit response models.
- Forgetting pagination on collection endpoints.
- Permissive CORS + credentials.
- Leaking sensitive fields or query parameters.
- Running with debug settings in production.
- Accepting past risk intentionally, but wanting to fail only on new regressions.
Philosophy: Security is a First‑Class Feature (Not an Optional Afterthought)
FastAPI (like most Python web frameworks) treats every new endpoint as open by default unless you remember to add a dependency, middleware, or explicit security layer. That default is great for exploration—but dangerous for serious applications where an unguarded route can leak data, expose internal mechanics, or become a lateral movement foothold.
This project flips that implicit default:
- Closed (or at least scrutinized) is the baseline; openness must be intentional and explicit.
- Any route lacking an accepted security dependency is treated as a potential regression unless you consciously accept it into the security baseline.
- Public endpoints are allowed—but must be justified (e.g. decorated with
@open_routeand/or accepted in the lock file).
Open by Accident vs. Open on Purpose
Implicitly exposed endpoints are a common source of:
- “Why is staging data visible in prod?” moments
- Shadow APIs (endpoints added in a refactor, never reviewed)
- Drift between expected security posture and reality
By making openness explicit, you reduce the cognitive load during review: it’s harder to smuggle in a silent, unsecured path.
The Baseline Lock File as a PR Gate
This mirrors how dependency lock files work: only deliberate changes modify the security posture. Accidental exposures surface immediately instead of months later in a pentest report.
Secure‑by‑Default Principles Enforced Here
- ✔️ Visibility: Every risk is enumerated at startup—no silent failures.
- ✔️ Friction for risk; low friction for safety: Secure endpoints pass quietly; new exposures demand action.
- ✔️ Deterministic strings: Findings are stable -> easy to review / baseline.
- ✔️ Intent markers:
@open_routevs@disable_security_checkscommunicate design decisions. - ✔️ Fail fast: CI / local dev breaks on unaccepted regressions.
- ✔️ Scalable: Add or drop checks without changing application logic.
If your application handles anything sensitive (users, tokens, internal metadata, usage analytics), default-open behavior is not good enough. Treat security posture the way you treat schema migrations or dependency bumps: explicit, reviewed, versioned.
Key Features
🔒 Baseline / Lock File (Core Feature)
Security findings as version-controlled artifacts:
- Accept existing tech debt while blocking new vulnerabilities
- Fail only on new findings — stabilizes CI without compromising security
- Diff-able PR changes — every new risk requires explicit approval
- Automatically updated with
SECURITY_BASELINE_UPDATE=1
🎯 Explicit Intent Markers
@open_route— marks intentionally public endpoints (auth check skipped, others still enforced)@disable_security_checks— disables all checks for exceptional routes (use sparingly)
📊 High-Value Security Checks
9 core checks covering critical security issues with low false positives:
- Missing auth dependencies (OWASP API2, API5)
- Response model & body validation (OWASP API3, API6)
- Sensitive field/query param exposure (OWASP API3)
- Unbounded pagination (OWASP API4)
- CORS & debug mode misconfigurations (OWASP API8)
- See Security Checks Reference for complete list
🔧 Extensible & Configurable
- Add/remove checks via constructor
- Write custom organizational rules
- Enable/disable heuristic checks
- Each check self-identifies with
CATEGORYandOWASPcodes
Installation & Usage
Installation
pip install fastapi-safeguard
Basic Usage
from fastapi import FastAPI
from fastapi_safeguard import FastAPISafeguard
app = FastAPI(lifespan=FastAPISafeguard.recommended().lifespan())
With Additional Heuristic Checks
The default includes only high-value checks. Add optional heuristic checks manually if needed:
from fastapi_safeguard import (
FastAPISafeguard,
HTTPSRedirectMiddlewareCheck,
RateLimitingPresenceCheck,
)
safeguard = FastAPISafeguard(checks=[
*FastAPISafeguard.recommended().checks,
HTTPSRedirectMiddlewareCheck(), # Often handled by infrastructure
RateLimitingPresenceCheck(), # Often external (API gateway)
])
app = FastAPI(lifespan=safeguard.lifespan())
Custom Configuration
from fastapi_safeguard import (
FastAPISafeguard,
DependencySecurityCheck,
ResponseModelSecurityCheck,
PaginationEnforcementCheck,
)
safeguard = FastAPISafeguard(checks=[
DependencySecurityCheck(
allowed_unsecured=["/openapi.json", "/docs", "/health"],
extra_dependencies=[my_custom_auth_dep] # accepts list or set
),
ResponseModelSecurityCheck(),
PaginationEnforcementCheck(),
])
app = FastAPI(lifespan=safeguard.lifespan())
Integration with Custom Lifespan
If your app already has a custom lifespan context manager, use run_checks() instead:
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi_safeguard import FastAPISafeguard
safeguard = FastAPISafeguard.recommended()
@asynccontextmanager
async def lifespan(app: FastAPI):
# Run security checks at startup
safeguard.run_checks(app)
# Your custom startup logic
print("Starting up with custom logic...")
db_connection = await setup_database()
yield # App is running
# Your custom shutdown logic
await db_connection.close()
print("Shutting down...")
app = FastAPI(lifespan=lifespan)
Security Checks Reference
✅ Core Checks (Included in Recommended Preset)
High-value checks with low false positives, included by default when using FastAPISafeguard.recommended():
| Check | Category | OWASP | Purpose | Example Finding |
|---|---|---|---|---|
| DependencySecurityCheck | auth | API2, API5 | Ensures at least one accepted auth/security dependency | GET /items has no accepted security dependency |
| UnsecuredAllowedMethodsCheck | auth | API5 | Prevents unsafe methods on declared unsecured paths | POST /status exposes unsafe method(s) without security |
| ResponseModelSecurityCheck | schema | API3 | Enforces explicit response models for unsafe methods | POST /users missing response_model... |
| BodyModelEnforcementCheck | schema | API3, API6 | Prevents raw dict/list bodies (mass assignment) | PATCH /users uses non-model raw body param(s): payload |
| SensitiveFieldExposureCheck | data_exposure | API3 | Flags sensitive-looking response fields | GET /auth/me response_model exposes potentially sensitive fields: password |
| SensitiveQueryParamCheck | data_exposure | API3 | Flags sensitive-looking query parameter names | GET /login exposes potentially sensitive data via query params: token |
| PaginationEnforcementCheck | performance | API4 | Promotes pagination controls on list endpoints | GET /reports returns a collection without pagination params |
| CORSMisconfigurationCheck | config | API8 | Detects overly permissive CORS with credentials | CORS misconfiguration: allow_origins='*', credentials allowed |
| DebugModeCheck | config | API8 | Fails if running with debug=True |
Application running in debug mode |
🔧 Optional Checks (Not Included by Default)
Available for manual addition. These have higher false positive rates or are often handled at infrastructure level:
| Check | Category | OWASP | Purpose | Why Not Default |
|---|---|---|---|---|
| HTTPSRedirectMiddlewareCheck | config | API8 | Suggests enforcing HTTPS redirect | Usually handled by load balancer/reverse proxy |
| TrustedHostMiddlewareCheck | config | API8 | Suggests host header protection | Usually handled upstream |
| RateLimitingPresenceCheck | performance | API4 | Heuristic: missing rate limiting middleware | Often external (API gateway, nginx), weak heuristic |
| ReturnTypeAnnotationCheck | schema | API3 | Encourages type annotations when no response_model | Too noisy, not a security issue |
| WildcardPathCheck | routing | API3, API5 | Warns on broad catch-all path params | Legitimate use cases (file serving) |
| DangerousMethodExposureCheck | http_methods | - | Flags TRACE/CONNECT methods | Rarely exposed via FastAPI, non-issue in practice |
| SSRFParameterCheck | ssrf | - | Detects query params like 'url', 'uri', 'target' | Too many false positives |
| AdminRouteOpenCheck | auth | - | Flags routes with '/admin' in path without deps | Weak heuristic based on path naming |
To use optional checks:
from fastapi_safeguard import FastAPISafeguard, HTTPSRedirectMiddlewareCheck
safeguard = FastAPISafeguard(checks=[
*FastAPISafeguard.recommended().checks,
HTTPSRedirectMiddlewareCheck(), # Add as needed
])
OWASP API Top 10 Coverage
Coverage by core checks only (included in recommended preset):
| OWASP Code | Risk (Short) | Core Checks | Optional Checks Available |
|---|---|---|---|
| API2 | Broken Authentication | DependencySecurityCheck | - |
| API3 | Excessive Data / Object Property Level Exposure | ResponseModelSecurityCheck, BodyModelEnforcementCheck, SensitiveFieldExposureCheck, SensitiveQueryParamCheck | ReturnTypeAnnotationCheck, WildcardPathCheck |
| API4 | Unrestricted Resource Consumption | PaginationEnforcementCheck | RateLimitingPresenceCheck |
| API5 | Broken Function Level Authorization | DependencySecurityCheck, UnsecuredAllowedMethodsCheck | WildcardPathCheck |
| API6 | Mass Assignment | BodyModelEnforcementCheck | - |
| API8 | Security Misconfiguration | CORSMisconfigurationCheck, DebugModeCheck | HTTPSRedirectMiddlewareCheck, TrustedHostMiddlewareCheck |
Note: Core checks provide solid coverage out-of-the-box. Optional checks can supplement coverage but have higher false positive rates.
Decorators
| Decorator | Effect | Still Enforces Other Checks? | Use Case |
|---|---|---|---|
@open_route |
Bypasses only the authentication dependency check | Yes | Public catalog, status page, landing page |
@disable_security_checks |
Skips all checks for that route | No | Internal metrics, intentionally exotic endpoint |
Example
from fastapi_safeguard import open_route, disable_security_checks
@open_route
@app.get("/public-catalog")
async def catalog():
"""Public endpoint - auth check skipped, other checks still enforced"""
return {"items": []}
@disable_security_checks
@app.get("/internal-metrics")
async def metrics():
"""All security checks disabled for this route"""
return {"cpu": 0.5}
Note: Decorator placement above @app.get/... is conventional but either order works; attributes attach to the underlying function.
Baseline (Lock) File Workflow
Think of security_baseline.json the same way you think of package-lock.json, poetry.lock, or Pipfile.lock — but instead of pinning dependency versions, it pins the exact set of currently accepted security findings.
If something new (an unsecured route, missing response model, etc.) appears, the diff to this file makes that change obvious in a pull request. No silent drift.
Mental Model
| You do this | The plugin does this |
|---|---|
| Run app first time (no baseline) | Scans, prints findings, exits with error (unless update flag supplied) |
| Accept current state | Writes a lock file snapshot of findings |
| Add a risky change | Fails fast: new finding not in baseline |
| Fix a finding | Old one stays in file until refreshed so you can see improvement diff |
| Refresh with update flag | Prunes resolved entries / accepts new ones |
Typical Flow
- First scan (will fail if there are findings):
uvicorn main:app --reload
- Accept the current snapshot (write / update lock file):
SECURITY_BASELINE_UPDATE=1 uvicorn main:app --reload # or in code: FastAPISafeguard(..., update_baseline=True)
- Subsequent runs now fail only if new findings appear:
uvicorn main:app --reload
- After fixing issues, prune resolved ones (removes them from the file):
SECURITY_BASELINE_UPDATE=1 uvicorn main:app --reload
- If there are zero findings and you refresh, the file becomes empty (clean slate).
Pull Request Review Shortcut
- Added lines in
security_baseline.json= new risks being accepted. - Removed lines = improvements / fixes.
- No file change = no security posture change.
When to Reject a Baseline Update
If a baseline diff shows something like:
+ "GET /internal-metrics has no accepted security dependency"
Ask: Was that endpoint meant to be open? If not, fix instead of accepting.
Quick Commands
# Fail on new findings only (normal dev / CI)
uvicorn main:app --reload
# Accept current findings (snapshot / refresh)
SECURITY_BASELINE_UPDATE=1 uvicorn main:app --reload
Treat the baseline like a living contract: change it only when you are consciously accepting or removing risk.
Startup Output Examples
With new findings (will fail)
Category Summary:
Category Total New Accepted OWASP
auth 1 1 0 API2/API5
schema 2 1 1 API3/API6
❌ Security check failed: new findings detected (not in baseline):
+ POST /users missing response_model for unsafe method(s)
+ GET /items has no accepted security dependency
To accept current findings run with SECURITY_BASELINE_UPDATE=1 or set update_baseline=True.
After acceptance
Category Summary:
Category Total New Accepted OWASP
auth 1 0 1 API2/API5
schema 2 0 2 API3/API6
✅ All security findings match accepted baseline (3 accepted).
Fully clean
✅ All security checks passed (0 findings, 12 routes, 14 checks).
Environment Variables
| Variable | Purpose | Default |
|---|---|---|
SECURITY_BASELINE_PATH |
Path to baseline file | security_baseline.json |
SECURITY_BASELINE_UPDATE |
If 1, accept current findings (write / refresh baseline) |
0 |
(Planned) Additional env toggles (see Roadmap) could include JSON output, category include/exclude filters, severity levels.
Configuration & Extensibility
safeguard = FastAPISafeguard(
checks=[
DependencySecurityCheck(extra_dependencies={my_custom_dep}),
ResponseModelSecurityCheck(),
PaginationEnforcementCheck(pagination_param_names=["limit", "offset"]),
# Add / remove checks freely
]
)
All checks expose constants:
CATEGORY: used for grouped summary.OWASP: list of relevant OWASP API Top 10 codes.
Remove noisy checks (e.g. heuristics) simply by omitting them from the list.
Writing a Custom Check
Minimal example:
from fastapi.routing import APIRoute
from fastapi_safeguard import SecurityCheck
class EnforceJsonCheck(SecurityCheck):
CATEGORY = "content"
OWASP = ["API8"] # misconfiguration flavor
def check_route(self, route: APIRoute):
if any("/legacy" in route.path for _ in [0]): # sample logic
return None
for response in route.response_models or []: # pseudocode
pass
# Return a finding string or None
return None
safeguard = FastAPISafeguard(checks=[EnforceJsonCheck(), ...])
Rules:
- Return
Nonewhen compliant. - Return short, stable strings for findings (baseline matches on exact text).
- Add
CATEGORY&OWASPfor better summaries.
To reuse skip semantics: subclass RouteCheck instead of SecurityCheck to inherit allowed_unsecured & @disable_security_checks behavior.
Ignoring / Accepting Findings
| Strategy | When to Use | Notes |
|---|---|---|
| Baseline accept | Existing known tech debt | Persists until you prune |
@open_route |
Legit public read-only endpoint | Still validated by other checks |
@disable_security_checks |
Exceptional / internal route | Use sparingly; documents intent |
| Remove check from config | Org chooses out-of-scope rule | Consider replacing with softer variant |
CI/CD Integration
Example GitHub Actions step:
- name: Security route check
run: |
pip install fastapi uvicorn
python -c "import main" # triggers lifespan startup logic
To accept current baseline in a controlled PR:
- name: Update baseline (opt-in)
if: github.event_name == 'workflow_dispatch'
run: |
SECURITY_BASELINE_UPDATE=1 python -c "import main"
git add security_baseline.json
git commit -m "chore: update security baseline" || echo "No changes"
Failing builds will show new findings distinctly (+ prefixed rows).
Performance Notes
- All checks execute once at startup; zero per-request overhead.
- Complexity roughly: O(routes × checks). Typical microservice: negligible (< few ms).
- Single-run checks short‑circuit after first execution.
Roadmap
- Severity levels (warn vs error) & downgrade mechanism.
- JSON output mode for machine parsing (SBOM / SARIF style).
- Category include / exclude filters (env-based).
@warn_onlydecorator (non-fatal for selected routes).- Scope / role enforcement scaffold (e.g.
@require_scopes([...])). - Packaged distribution (PyPI) with plugin discovery entry points.
- Optional HTML or Markdown report emitter.
Contributing
- Fork & branch (
feat/my-improvement). - Keep PRs focused (one conceptual change).
- Add or update README examples if behavior / options change.
- Prefer small, composable checks over giant multi-purpose ones.
- Use clear finding strings (deterministic, no timestamps or randomness).
Feel free to open an issue proposing new categories or OWASP mappings first.
Security Policy
Found a security issue with the plugin itself (e.g., code execution via baseline parsing)?
- Please open a GitHub issue or contact the maintainer
- For sensitive security issues, avoid including exploit details in public issues
FAQ
Q: Does this replace runtime authorization?
A: No. It prevents common omissions early. You still need robust runtime auth.
Q: Will it block production if I forget to update the baseline?
A: Yes—intentionally. Incorporate baseline refresh into the PR that introduces the accepted risk.
Q: Can I have different baselines per environment?
A: Yes—point SECURITY_BASELINE_PATH at different files (e.g. security_baseline.dev.json).
Q: Are findings deduplicated?
A: Yes—each route/check pair should produce at most one stable string.
Q: How do I silence RateLimitingPresenceCheck but keep others?
A: Omit it from the checks=[...] list when constructing FastAPISafeguard.
Q: What if I refactor a route path?
A: The old finding becomes resolved; run with SECURITY_BASELINE_UPDATE=1 to prune it.
License
MIT License - see LICENSE file for details.
Happy hardening!
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastapi_safeguard-0.1.0.tar.gz.
File metadata
- Download URL: fastapi_safeguard-0.1.0.tar.gz
- Upload date:
- Size: 26.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
44696fa6e0e8b57e02a8c70348424415485b99d8c4ec9733214a28ebf3f723aa
|
|
| MD5 |
ad903572dda19f894d075f611cd13f2c
|
|
| BLAKE2b-256 |
b0d426373d433936fb18cab127dea765f2f00296faea6c77e85dbab94e71812c
|
File details
Details for the file fastapi_safeguard-0.1.0-py3-none-any.whl.
File metadata
- Download URL: fastapi_safeguard-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
06bd20d545a53805b14ed2d317eff8db7f199bdaa186a04f90375baf8ff72f68
|
|
| MD5 |
8d36d79104f8320656e7cebed6dd814f
|
|
| BLAKE2b-256 |
2db249a9a918d6837dd529833a08a3f556850d9559981c2b62de0035004a01a7
|