agentsclimarketplace

Fastapi security scan

Skill Dolphinllc/claude-security-skills/skills/defensive/web/fastapi-security-scan

Defensive security skills for Claude Code and the Claude Agent SDK — web applications and generative AI systems.

Install
npx -y skills add Dolphinllc/claude-security-skills --skill fastapi-security-scan

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Defensive security scan for FastAPI applications. Detects missing Depends/Security guards, Pydantic validation bypasses, permissive CORS, unverified JWTs, raw SQL string interpolation, and unsafe file responses. Invoke when the user asks to "review", "audit", or "scan" a FastAPI project, or when editing routers and dependencies.

SKILL.md

5.4 KB, as published. Nobody here has run it

FastAPI Security Scan

Defensive scan for FastAPI projects (FastAPI 0.100+, Pydantic v2). Reports findings using the shared scoring schema.

Scope

  • **/*.py files containing APIRouter, FastAPI, or Depends
  • main.py / app factory
  • pyproject.toml for known-vulnerable pins

Out of scope: deployment (uvicorn/gunicorn flags), dependency CVEs.

Procedure

  1. Locate the FastAPI() instance and all APIRouter instances.
  2. For each route function, walk parameters and decorators.
  3. Apply rules below. Emit findings in the shared schema.

Rules

IDSeverityDetectionFix
FASTAPI-AUTH-001criticalMutating route (POST/PUT/PATCH/DELETE) with no Depends/Security parameter referring to an auth functionAdd current_user: User = Depends(get_current_user)
FASTAPI-AUTH-002highAuth dependency exists but does not raise on missing/invalid token (returns None and route doesn't check)Raise HTTPException(401) inside the dependency
FASTAPI-JWT-001criticaljwt.decode(..., options={"verify_signature": False}) or jwt.decode without algorithms=Always pass algorithms=["RS256"] (or your alg); never disable signature verification
FASTAPI-JWT-002highJWT decode without audience= / issuer= checksPass audience and issuer explicitly
FASTAPI-PYD-001highRoute function takes dict or Any as body parameter (bypasses Pydantic validation)Define a Pydantic BaseModel and use it as the parameter type
FASTAPI-PYD-002mediumPydantic model uses model_config = ConfigDict(extra="allow") on input boundaryUse extra="forbid" for inbound payloads
FASTAPI-CORS-001highCORSMiddleware with allow_origins=["*"] and allow_credentials=TruePin origins to an allowlist when credentials are allowed
FASTAPI-CORS-002mediumCORSMiddleware with allow_methods=["*"] and allow_headers=["*"] on auth-sensitive routesEnumerate explicit methods/headers
FASTAPI-SQL-001criticalf-string / %-formatting / .format() building SQL passed to execute() / text()Use parameterized queries: text("SELECT … WHERE id=:id"), {"id": id}
FASTAPI-SQL-002highSQLAlchemy Session.execute(text(user_input)) without bind paramsUse bind params or ORM constructs
FASTAPI-FILE-001highFileResponse(path) where path is built from request input without pathlib.Path.resolve() containment checkResolve under a fixed base dir and verify with is_relative_to
FASTAPI-DBG-001highapp = FastAPI(debug=True) in production code path, or --reload defaulted onDefault debug=False; gate behind env var
FASTAPI-EXC-001mediumCustom exception handler returning repr(exc) / traceback.format_exc() to clientsLog details server-side; return generic message to client
FASTAPI-RATE-001mediumNo rate-limiter (slowapi, fastapi-limiter) on /login, /register, /forgot-passwordApply per-IP limiter
FASTAPI-PWD-001highPassword compared with == or hashed with hashlib.md5/sha1/sha256 directlyUse passlib[bcrypt] or argon2-cffi

Wrong vs. right

FASTAPI-AUTH-001 (missing dependency)

# ❌ Anyone can delete any user
@router.delete("/users/{user_id}")
async def delete_user(user_id: int):
    await users.delete(user_id)
# ✅ Auth dependency + ownership check
@router.delete("/users/{user_id}")
async def delete_user(
    user_id: int,
    current_user: User = Depends(get_current_user),
):
    if current_user.id != user_id and not current_user.is_admin:
        raise HTTPException(status_code=403)
    await users.delete(user_id)

FASTAPI-JWT-001 (signature verification disabled)

# ❌ Anyone can mint tokens
payload = jwt.decode(token, options={"verify_signature": False})
# ✅ Verified
payload = jwt.decode(
    token,
    PUBLIC_KEY,
    algorithms=["RS256"],
    audience="my-api",
    issuer="https://issuer.example.com",
)

FASTAPI-SQL-001 (string-formatted SQL)

# ❌ Injection
await session.execute(text(f"SELECT * FROM users WHERE email = '{email}'"))
# ✅ Parameterized
await session.execute(
    text("SELECT * FROM users WHERE email = :email"),
    {"email": email},
)

FASTAPI-FILE-001 (path traversal)

# ❌ ../../etc/passwd
@router.get("/files/{name}")
async def get_file(name: str):
    return FileResponse(f"/var/uploads/{name}")
# ✅ Containment check
BASE = Path("/var/uploads").resolve()

@router.get("/files/{name}")
async def get_file(name: str):
    target = (BASE / name).resolve()
    if not target.is_relative_to(BASE) or not target.is_file():
        raise HTTPException(404)
    return FileResponse(target)

References

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.