agentsclimarketplace

Security

Skill MARUCIE/openclaw-foundry/web/public/packs/spellbook-security-auditor/skills/security

The curated AI Agent skill marketplace — 37K+ vetted skills, S/A/B/C ratings, deploy anywhere

Install
npx -y skills add MARUCIE/openclaw-foundry --skill security

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

Use when implementing authentication, authorization, input validation, or secrets management — or when auditing an existing service against OWASP Top 10 risks, configuring security headers, or running a STRIDE threat model.

SKILL.md

28.2 KB, as published. Nobody here has run it

是什么

这是一份后端安全综合规范,覆盖鉴权、授权、输入校验、密钥管理、OWASP(开放式 Web 应用安全项目)Top 10 风险、STRIDE(六类威胁建模方法)威胁建模、安全响应头等核心控制点,让团队从写代码起就具备防护意识,而不是上线后再补救。

怎么用

  1. 新功能立项时按 STRIDE 框架做威胁建模,把潜在攻击面在设计阶段就列清楚。
  2. 写鉴权和会话管理代码时套用文档的标准模板,密码哈希、Token 签发、退出失效一次到位。
  3. 处理用户输入时按白名单校验、参数化查询、输出编码三件套防御,杜绝注入和 XSS(跨站脚本攻击)。
  4. 上线前对照 OWASP Top 10 清单逐项自检,特别是访问控制、安全配置、依赖漏洞三大重灾区。
  5. 配置 Content-Security-Policy(内容安全策略)等安全响应头时按文档基线套用,逐步收紧不一刀切。

架构图

flowchart LR
    A[功能设计] --> B[STRIDE 建模]
    B --> C[安全编码]
    C --> D[OWASP 自检]
    D --> E[依赖扫描]
    E --> F[上线放行]

Security

A comprehensive reference for securing backend APIs and web applications — covering authentication, authorization, secrets, input validation, security headers, threat modeling, and dependency hygiene across Python, TypeScript, and Go.

When to Activate

  • Implementing authentication or session management
  • Reviewing code for security vulnerabilities or injection risks
  • Handling secrets, API keys, or credentials in code or config
  • Designing authorization — who can access what
  • Configuring security headers for an API or web application
  • Setting up dependency scanning in CI
  • Running a threat model for a new feature or system

OWASP Top 10 Quick Reference

#VulnerabilityDescriptionPrimary Mitigation
A01Broken Access ControlUsers act outside intended permissionsEnforce ownership checks server-side on every request
A02Cryptographic FailuresSensitive data exposed due to weak/absent encryptionTLS everywhere, strong hashing, encrypted secrets at rest
A03InjectionHostile data sent to an interpreterParameterized queries, input validation, output encoding
A04Insecure DesignMissing or ineffective security controls in the designThreat modeling, secure design patterns, abuse case review
A05Security MisconfigurationInsecure defaults, open cloud storage, verbose errorsHarden configs, disable defaults, suppress stack traces in prod
A06Vulnerable ComponentsUsing components with known vulnerabilitiesDependency scanning in CI, automated updates via Renovate/Dependabot
A07ID & Auth FailuresWeak authentication, credential stuffing, broken session managementMFA, secure password hashing, session invalidation, rate limiting
A08Software/Data Integrity FailuresUnsigned updates, insecure deserialization, CI/CD tamperingVerify supply chain, sign artifacts, restrict pipeline permissions
A09Logging & Monitoring FailuresInsufficient logging to detect breachesStructured audit logs, alerting on anomalies, log retention policy
A10SSRFServer fetches attacker-controlled URLAllowlist destinations, block internal address ranges (169.254.x.x, 10.x.x.x)

Injection (A03) — Deep Dive

Never construct queries or commands by concatenating user input.

SQL Injection

# BAD — string concatenation opens SQLi
query = f"SELECT * FROM users WHERE email = '{user_input}'"
cursor.execute(query)

# GOOD — parameterized query
cursor.execute("SELECT * FROM users WHERE email = %s", (user_input,))
// BAD
const rows = await db.query(`SELECT * FROM users WHERE email = '${userInput}'`);

// GOOD
const rows = await db.query("SELECT * FROM users WHERE email = $1", [userInput]);
// BAD
row := db.QueryRow("SELECT * FROM users WHERE email = '" + userInput + "'")

// GOOD
row := db.QueryRow("SELECT * FROM users WHERE email = ?", userInput)

Command Injection

# BAD — shell=True with user input is always dangerous
import subprocess
subprocess.run(f"convert {filename}", shell=True)

# GOOD — pass args as a list, shell=False (default)
subprocess.run(["convert", filename])
// BAD
exec(`convert ${filename}`);

// GOOD — use execFile with an array of arguments
import { execFile } from "child_process";
execFile("convert", [filename]);
// BAD
cmd := exec.Command("sh", "-c", "convert "+filename)

// GOOD
cmd := exec.Command("convert", filename)

Broken Access Control (A01) — Deep Dive

Check ownership at the handler level, not only role membership. A role check alone prevents vertical privilege escalation (a regular user doing admin things) but not horizontal privilege escalation (a user accessing another user's resources — IDOR).

IDOR (Insecure Direct Object Reference)

# BAD — checks auth, but not ownership
@app.get("/invoices/{invoice_id}")
def get_invoice(invoice_id: int, current_user=Depends(get_current_user)):
    return db.query(Invoice).filter(Invoice.id == invoice_id).first()

# GOOD — verifies the resource belongs to the caller
@app.get("/invoices/{invoice_id}")
def get_invoice(invoice_id: int, current_user=Depends(get_current_user)):
    invoice = db.query(Invoice).filter(
        Invoice.id == invoice_id,
        Invoice.owner_id == current_user.id,  # ownership check
    ).first()
    if not invoice:
        raise HTTPException(status_code=404)
    return invoice
Escalation TypeDescriptionExample
VerticalLower-privileged user accesses higher-privileged functionsRegular user calls admin endpoint
HorizontalUser accesses another user's data at the same privilege levelUser A reads User B's invoices via IDOR

Security Misconfiguration (A05) — Deep Dive

  • Remove default credentials immediately (database root passwords, admin/admin panels).
  • Suppress stack traces in production — return generic error messages, log details server-side.
  • Audit S3 bucket ACLs: aws s3api get-bucket-acl --bucket <name> — buckets must never be public-read or public-read-write unless serving static public assets intentionally.
  • CORS must allowlist specific origins; Access-Control-Allow-Origin: * disables same-origin protection for all browsers.

Authentication Patterns

Password Hashing

Never store plaintext passwords or hashes produced by MD5, SHA-1, or unsalted SHA-256. Use a slow, adaptive hashing algorithm: bcrypt (work factor ≥ 12) or argon2id.

# Python — bcrypt via passlib
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto", bcrypt__rounds=12)

hashed = pwd_context.hash("user_plaintext_password")
valid  = pwd_context.verify("user_plaintext_password", hashed)
// TypeScript — bcryptjs
import bcrypt from "bcryptjs";

const hashed = await bcrypt.hash(plaintext, 12);       // work factor 12
const valid  = await bcrypt.compare(plaintext, hashed);
// Go — golang.org/x/crypto/bcrypt
import "golang.org/x/crypto/bcrypt"

hashed, err := bcrypt.GenerateFromPassword([]byte(plaintext), 12)
err = bcrypt.CompareHashAndPassword(hashed, []byte(plaintext))

JWT (JSON Web Tokens)

Structure: Base64URL(header).Base64URL(payload).signature

Signing algorithm choice:

AlgorithmKey TypeUse Case
RS256RSA key pair (asymmetric)Microservices — verify without sharing secret; preferred
HS256Shared secret (symmetric)Single-service — simpler, but secret must be shared to verify
ES256ECDSA key pair (asymmetric)Same as RS256 but smaller tokens

Required claims: iss (issuer), sub (subject/user ID), aud (audience), exp (expiry unix timestamp), iat (issued-at unix timestamp).

Token storage:

<!-- BAD — localStorage is readable by any JavaScript on the page (XSS risk) -->
localStorage.setItem("access_token", token);

<!-- GOOD — httpOnly cookie is inaccessible to JavaScript -->
<!-- Set by server: Set-Cookie: access_token=<jwt>; HttpOnly; Secure; SameSite=Strict -->

Refresh token rotation: Issue a short-lived access token (15 min) alongside a long-lived refresh token (7 days). On each refresh, invalidate the old refresh token and issue a new one. Detect token reuse — if a refresh token is used twice, revoke the entire family.

JWT verification — all 3 languages:

# Python — PyJWT
import jwt

payload = jwt.decode(
    token,
    public_key,           # RS256: public key; HS256: shared secret
    algorithms=["RS256"],
    audience="my-api",
    options={"require": ["exp", "iss", "sub", "aud"]},
)
// TypeScript — jose
import { jwtVerify } from "jose";

const { payload } = await jwtVerify(token, publicKey, {
  issuer: "https://auth.example.com",
  audience: "my-api",
});
// Go — golang-jwt/jwt
import "github.com/golang-jwt/jwt/v5"

token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
    if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
        return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
    }
    return publicKey, nil
}, jwt.WithAudience("my-api"), jwt.WithIssuer("https://auth.example.com"))

Session-Based Auth

For server-rendered applications, server-side sessions (stored in Redis or a database) are a simpler and often safer alternative to JWTs.

Set all three protective cookie flags:

Set-Cookie: session_id=<opaque_id>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
FlagEffect
HttpOnlyCookie inaccessible to JavaScript — blocks XSS token theft
SecureCookie sent only over HTTPS
SameSite=StrictCookie not sent on cross-site requests — blocks CSRF

OAuth2 / OIDC

Flow Selection

FlowUse CasePKCE Required
Authorization Code + PKCEBrowser SPA, mobile/native appsYes
Client CredentialsMachine-to-machine (M2M), backend servicesNo — uses client secret
Device CodeCLI tools, smart TV appsNo — polling-based

Authorization Code + PKCE Steps

  1. App generates a cryptographically random code_verifier (43–128 chars).
  2. App computes code_challenge = BASE64URL(SHA256(code_verifier)).
  3. App redirects user to IdP authorization URL with code_challenge and code_challenge_method=S256.
  4. User authenticates at IdP; IdP redirects back with authorization_code.
  5. App exchanges authorization_code + code_verifier (not the challenge) at token endpoint.
  6. IdP verifies SHA256(code_verifier) == code_challenge stored from step 3, then returns tokens.
  7. App stores access token in httpOnly cookie; never in localStorage.

Build vs Buy

Use a third-party IdP (Auth0, Clerk, Cognito, Okta) unless you have exceptional requirements. Rolling your own OAuth2/OIDC server is a multi-month project with serious security risk surface: token endpoint, PKCE, refresh rotation, MFA, brute-force protection, RBAC, and audit logging all need to be correct simultaneously.

Scope Design

# BAD — wildcard and coarse scopes leak excessive access
scopes: ["admin", "*", "readwrite"]

# GOOD — narrow, resource-specific scopes
scopes: ["read:invoices", "write:invoices", "read:profile"]

Never grant admin or wildcard scopes to third-party integrations. Use the principle of least privilege — request only the scopes needed for the operation.

Authorization Patterns

RBAC vs ABAC

DimensionRBACABAC
DefinitionPermissions assigned to roles; users assigned to rolesPermissions derived from attributes of user, resource, environment
ComplexityLow — simple to implement and auditHigh — policy engine required (OPA, Casbin)
FlexibilityLow — coarse-grained; role explosion at scaleHigh — fine-grained contextual decisions
Best ForSmall teams, well-defined job functionsMulti-tenant SaaS, dynamic access policies, data-level isolation

Resource-Level Authorization

Check ownership in the handler, not only in middleware. Middleware can verify the token is valid and extract the role — it cannot verify the requested resource belongs to the caller.

// BAD — only checks role, not resource ownership
router.delete("/posts/:id", requireRole("user"), async (req, res) => {
  await db.posts.delete({ where: { id: req.params.id } });
  res.sendStatus(204);
});

// GOOD — verifies caller owns the resource before deleting
router.delete("/posts/:id", requireRole("user"), async (req, res) => {
  const post = await db.posts.findUnique({ where: { id: req.params.id } });
  if (!post) return res.sendStatus(404);
  if (post.authorId !== req.user.id) return res.sendStatus(403); // ownership check
  await db.posts.delete({ where: { id: req.params.id } });
  res.sendStatus(204);
});

Policy Object Pattern

For complex logic, encapsulate authorization decisions in a policy object rather than scattering if checks across handlers.

class PostPolicy:
    def can_edit(self, user, post) -> bool:
        return post.author_id == user.id or user.role == "admin"

    def can_delete(self, user, post) -> bool:
        return post.author_id == user.id or user.role == "admin"

    def can_publish(self, user, post) -> bool:
        return user.role in ("editor", "admin")

# Handler
policy = PostPolicy()
if not policy.can_edit(current_user, post):
    raise PermissionError("Forbidden")

Common Authorization Mistakes

  • Checking permissions only in the UI — API endpoints are directly reachable; always enforce server-side.
  • Enforcing access control on reads but not on writes — verify on every create, update, and delete.
  • Relying on obscurity — never assume an endpoint is safe because it is undocumented.
  • Missing re-authorization after role changes — invalidate sessions or tokens when a user's role is downgraded.

Secrets Management

The rule: never commit secrets. A .env file is for local development only — add it to .gitignore and never commit it to version control.

# BAD — hardcoded secret in source code
SECRET_KEY = "sk_live_abc123supersecretkey"
DATABASE_URL = "postgres://admin:password123@prod-db/app"

# GOOD — read from environment at runtime
import os
SECRET_KEY  = os.environ["SECRET_KEY"]
DATABASE_URL = os.environ["DATABASE_URL"]

Secrets Progression by Environment

EnvironmentSecret StorageNotes
Local Dev.env file (gitignored)Never commit; use .env.example with fake values for documentation
CI (GitHub Actions)Repository or org Secrets (${{ secrets.NAME }})Masked in logs; scoped to workflows
CI (GitLab)CI/CD Variables (masked, protected)Mark variables as masked to prevent log exposure
ProductionHashiCorp Vault / AWS Secrets Manager / GCP Secret Manager / DopplerDynamic secrets, audit trail, automatic rotation

Secret Rotation

  • Never reuse secrets across services or environments.
  • Rotate immediately on suspected exposure — treat exposure as confirmed until proven otherwise.
  • Automate rotation where the provider supports it (AWS RDS credentials via Secrets Manager, Vault dynamic secrets).
  • After rotation, verify all consumers are updated before invalidating the old secret.

Required .gitignore Entries

.env
.env.*
!.env.example
*.pem
*.key
*.p12
credentials.json
serviceAccountKey.json
.aws/credentials
kubeconfig
terraform.tfvars

Input Validation and Injection Prevention

Parameterized Queries

Always use parameterized queries or an ORM with parameter binding. No exceptions for "safe" input — the validation layer can be bypassed.

# BAD
cursor.execute("INSERT INTO orders (user_id, item) VALUES ('" + user_id + "', '" + item + "')")

# GOOD — psycopg2 / SQLAlchemy
cursor.execute("INSERT INTO orders (user_id, item) VALUES (%s, %s)", (user_id, item))
// BAD
await pool.query(`INSERT INTO orders (user_id, item) VALUES ('${userId}', '${item}')`);

// GOOD — pg / Prisma / Drizzle
await pool.query("INSERT INTO orders (user_id, item) VALUES ($1, $2)", [userId, item]);
// BAD
db.Exec("INSERT INTO orders (user_id, item) VALUES ('" + userID + "', '" + item + "')")

// GOOD
db.Exec("INSERT INTO orders (user_id, item) VALUES (?, ?)", userID, item)

File Upload Validation

import magic  # python-magic (libmagic binding)

ALLOWED_TYPES = {"image/jpeg", "image/png", "application/pdf"}
MAX_SIZE_BYTES = 10 * 1024 * 1024  # 10 MB

def validate_upload(file_bytes: bytes, claimed_extension: str) -> None:
    if len(file_bytes) > MAX_SIZE_BYTES:
        raise ValueError("File too large")
    detected_mime = magic.from_buffer(file_bytes, mime=True)
    if detected_mime not in ALLOWED_TYPES:
        raise ValueError(f"File type not allowed: {detected_mime}")
    # Store to a path outside the web root — never serve raw uploads directly

Key rules:

  • Detect MIME type from file content (magic bytes), not from the file extension or Content-Type header.
  • Enforce a maximum size before reading the full payload into memory.
  • Store uploaded files outside the web root or in object storage, never in a publicly served directory.
  • Rename files on storage — never preserve the user-supplied filename.

HTML Output Escaping

// BAD — executes arbitrary JS if userInput contains <script>...</script>
element.innerHTML = userInput;

// GOOD — use textContent for plain text
element.textContent = userInput;

// GOOD — for rich HTML, use a sanitizer library
import DOMPurify from "dompurify";
element.innerHTML = DOMPurify.sanitize(userInput);

Use auto-escaping templating engines (Jinja2, Go html/template, React JSX) — they escape by default. Only bypass escaping intentionally with | safe / dangerouslySetInnerHTML and only on content you fully control.

Validation Libraries

LanguageLibraryNotes
PythonpydanticSchema-first; validates at parse time; ideal for FastAPI
TypeScriptzodRuntime schema validation; pairs well with tRPC
Gogo-playground/validatorStruct tag–based validation; widely used with Gin/Echo

Validate at the API boundary — reject invalid input before it reaches business logic or the database.

Security Headers

Set these headers on every HTTP response from your API or web server.

HeaderRecommended ValueWhat It Prevents
Content-Security-Policydefault-src 'self'; script-src 'self'; object-src 'none'XSS, data injection, resource hijacking
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preloadSSL stripping, MITM downgrade attacks
X-Frame-OptionsDENYClickjacking via iframe embedding
X-Content-Type-OptionsnosniffMIME-type sniffing attacks
Referrer-Policystrict-origin-when-cross-originReferrer URL leakage to third parties
Permissions-Policycamera=(), microphone=(), geolocation=()Unauthorized browser feature access
Cache-Controlno-store (for authenticated endpoints)Sensitive data cached in shared proxies

CORS configuration:

# BAD — allows any origin to make credentialed requests
CORS(app, origins="*", supports_credentials=True)

# GOOD — explicit allowlist; never wildcard for authenticated routes
CORS(app, origins=["https://app.example.com", "https://admin.example.com"])

Access-Control-Allow-Origin: * is acceptable only for fully public, unauthenticated endpoints (e.g., a public API with no user data). Never combine * with Access-Control-Allow-Credentials: true — browsers reject it, and it would be a severe security flaw if they did not.

Adding Headers in Each Language

# FastAPI — middleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from starlette.middleware.base import BaseHTTPMiddleware

class SecurityHeadersMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        response = await call_next(request)
        response.headers["X-Content-Type-Options"] = "nosniff"
        response.headers["X-Frame-Options"] = "DENY"
        response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
        return response
// Express — use helmet
import helmet from "helmet";
app.use(helmet());  // sets all recommended headers with sane defaults
// Go — net/http middleware
func securityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        next.ServeHTTP(w, r)
    })
}

STRIDE Threat Modeling

STRIDE Reference Table

ThreatDescriptionMitigation Category
SpoofingClaiming an identity you do not ownAuthentication — verify identity on every request
TamperingModifying data in transit or at rest without authorizationIntegrity — TLS, HMAC signing, database audit logs
RepudiationDenying that an action occurredAudit logging — immutable, attributable event records
Information DisclosureExposing sensitive data to unauthorized partiesEncryption at rest and in transit, access control, minimal data exposure
Denial of ServiceMaking the system unavailable to legitimate usersRate limiting, resource quotas, circuit breakers, auto-scaling
Elevation of PrivilegeGaining capabilities beyond what is authorizedAuthorization checks on every operation, principle of least privilege

Threat Modeling Process

  1. Draw a data flow diagram (DFD). Identify every external entity, process, data store, and data flow in the feature.
  2. Mark trust boundaries. A trust boundary is crossed wherever data moves between different trust levels — browser to API, API to database, service to third-party, internal service to internal service.
  3. Apply STRIDE to each data flow and process. For each element, ask: can it be Spoofed? Tampered? Repudiated? Disclosed? Denied? Escalated?
  4. List mitigations. For each identified threat, record the control that addresses it (or note that it is accepted risk with justification).
  5. Re-review when the design changes. A threat model is not a one-time artifact — revisit it when auth, data flows, or trust boundaries change.

Lightweight threat model template:

Data FlowSTRIDE Threats IdentifiedMitigation
Browser → API (login)Spoofing (credential stuffing), DoS (brute force)Rate limit login endpoint, MFA, account lockout
API → DatabaseInjection (A03), Information DisclosureParameterized queries, least-privilege DB user
API → Third-party payment serviceTampering (webhook), RepudiationVerify webhook HMAC signature, log all events
S3 presigned URL → BrowserInformation DisclosureShort-lived URLs (15 min), bucket policy denies public access

Dependency Scanning

Tools by Language

LanguageToolPurpose
Pythonpip-auditScans installed packages against OSV/PyPI advisory database
PythonsafetyCVE scanning; integrates with CI
PythonDependabotAutomated PRs for vulnerable package updates
TypeScriptnpm auditBuilt-in; checks against npm advisory database
TypeScriptsocket.devSupply chain analysis — detects typosquatting and malicious packages
TypeScriptSnykCVE scanning with fix PRs
GogovulncheckOfficial Go vulnerability scanner from the Go team; checks call graph

CI Integration

# GitHub Actions — Python
- name: Scan dependencies
  run: pip-audit --strict --vulnerability-service osv

# GitHub Actions — TypeScript
- name: Scan dependencies
  run: npm audit --audit-level=high

# GitHub Actions — Go
- name: Scan dependencies
  run: |
    go install golang.org/x/vuln/cmd/govulncheck@latest
    govulncheck ./...

Fail the CI build on HIGH or CRITICAL severity findings. Treat a new HIGH CVE in a direct dependency the same as a failing test — it blocks the merge.

Automated Dependency Updates

Configure Dependabot (/.github/dependabot.yml) or Renovate to open automated PRs when dependency updates are available. Keep the update cadence short (weekly) to avoid large, risky batch upgrades.

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: pip
    directory: "/"
    schedule:
      interval: weekly
  - package-ecosystem: npm
    directory: "/"
    schedule:
      interval: weekly
  - package-ecosystem: gomod
    directory: "/"
    schedule:
      interval: weekly

SBOM Generation

Generate a Software Bill of Materials for auditing and compliance.

# Python
pip install cyclonedx-bom
cyclonedx-py environment -o sbom.json

# TypeScript
npx @cyclonedx/cyclonedx-npm --output-file sbom.json

# Go
go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@latest
cyclonedx-gomod app -output sbom.json

Red Flags

  • JWT validated on signature only, not expiry or audience — a valid signature on an expired or wrong-audience token still passes; always check exp, nbf, and aud claims
  • RBAC without object-level ownership check — checking role but not resource ownership lets any admin-role user access other users' data; always verify resource.owner_id == current_user.id
  • Input validation inside business logic — validate at the request boundary (controller/handler); by the time data reaches domain logic it should already be trusted and clean
  • STRIDE skipped for "small" features — new auth paths, file uploads, and webhooks introduce entire attack classes; STRIDE at design time costs minutes, post-incident costs days
  • Dependency scanning only at release — new CVEs are published daily; run dep scanning on every PR and on a nightly schedule against the default branch
  • Cookie flags missing on session tokens — without HttpOnly, Secure, and SameSite=Strict, session cookies are vulnerable to XSS theft and CSRF; set all three
  • Auth middleware applied to route groups but not verified per handler — a misconfigured route group silently bypasses middleware; verify auth and ownership explicitly in each sensitive handler

Checklist

  • Passwords hashed with bcrypt (factor 12+) or argon2id — never MD5/SHA1/plaintext
  • JWT signed with RS256 (asymmetric) or HS256 with strong secret (32+ random bytes)
  • Access tokens short-lived (15 min max); refresh tokens rotate on every use
  • Secrets stored in vault/secrets manager in production — never in env files committed to git
  • .gitignore covers .env, *.pem, *.key, credentials.json, serviceAccountKey.json
  • All SQL queries use parameterized statements — no string concatenation with user input
  • User input validated at the API boundary with pydantic / zod / go-playground/validator
  • Authorization checks resource ownership (resource.owner_id == current_user.id), not just role
  • Authorization enforced on create, update, and delete — not only on read
  • Security headers set on all API and web responses (CSP, HSTS, X-Frame-Options, nosniff)
  • CORS configured to allowlist specific origins — no wildcard * on authenticated routes
  • Dependency scanner runs in CI and fails the build on HIGH/CRITICAL CVEs
  • No stack traces or internal error details exposed in production API responses
  • File uploads validated by MIME type (magic bytes), size limit enforced, stored outside web root
  • Threat model reviewed for any new feature touching authentication, authorization, or sensitive data

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.