Security review
A portable Claude Code skills library (plugin) for a Django 5.2 + Next.js 16 house stack: 30 model-agnostic, tenant-isolation-and-security-first skills.
npx -y skills add Deadlymind/nanolama --skill security-reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 21 days oldThe repository was created 21 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 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
Audits a diff for this stack's top risks — tenant isolation on every business queryset (fail-closed, no client-supplied tenant id trusted), RBAC on every custom endpoint and @action, PII leakage in list endpoints/logs/exports, file-upload validation (content-type, size, extension, private storage), and secrets not committed. Use when reviewing a Django/DRF change before merge, auditing a new ViewSet or upload handler, checking a PR for security issues, or asked "is this safe to ship". Findings are confidence-gated. Run only on TRUSTED diffs — not hardened against prompt injection. Not for generic correctness/quality review (see code-review) or proving behavior works (see verify).
SKILL.md
7.2 KB, as published. Nobody here has run it
Security review (audit changes for top risks)
When to use
Reviewing a diff before merge on this stack, especially one that touches a
ViewSet, a custom @action, a serializer, an upload handler, an export, or
logging. Focus on the handful of risks that actually bite this architecture,
not a generic OWASP recital.
CAUTION — trusted diffs only. This skill reads code and follows its intent. It is not hardened against prompt injection. Do not run it on untrusted or attacker-controlled diffs; a hostile comment or string could steer the review.
Pattern
Walk the diff against a fixed checklist, and gate every finding by confidence: report Confirmed (you can point at the vulnerable line and the exploit), Likely (pattern is present but the sink is one hop away), or Question (needs the author to confirm). Never pad the report with speculative low-signal noise — a fail-closed tenant leak outranks a style nit by orders of magnitude.
The checklist
- Tenant isolation. Every business queryset scoped by the tenant FK
(
entreprise) and fail-closed — no tenant resolves to.none(), never all rows. The tenant comes fromrequest.user, never from a body/query param. Flag any rawModel.objects.get/filter(...)in a view that bypasses the scoped queryset, and any@actionthat re-queries without scoping (seemulti-tenancy). - RBAC. Every custom endpoint and every
@actioncarries an explicit permission — no endpoint relies on "logged in" alone. A new action without a{resource}_{action}gate is a finding (seerbac-permissions). - PII leakage. List/export serializers must not spill emails, phones, tokens, or internal ids beyond what the screen needs; logs and error payloads must not echo request bodies or secrets.
- File uploads. Validate content-type and extension and size; store to private storage (never a public-read S3 ACL); never trust the client filename.
- Secrets. No keys, tokens, or
.envvalues in the diff — config comes from the environment. Watch AI/search keys especially (seeai-integration). - Money & counters. Flag any balance/counter that reads a value into app code
and writes it back. A read-modify-write without
select_for_update()insidetransaction.atomic()is a race. Confirmed example: two concurrent requests both read100, both subtract, one write is lost. Require an atomicF()-style expression, a decimal type for money, and validation inside the locked block — an "insufficient funds" check outside the lock still oversells (seemoney-decimal,db-concurrency). - Object-level authz / BOLA. Validate every foreign-key id in the body or
params against the caller's tenant, not just the object being fetched. An
unvalidated FK lets a user reference or attach another tenant's row even when the
top-level queryset looks correctly scoped — e.g. posting a
category_idthat belongs to a different tenant. Re-scope each FK through the caller's queryset. - Auth & tokens. Tokens live in
HttpOnly/Secure/SameSitecookies, neverlocalStorage. Never log a token, cookie, orAuthorizationheader. No hand-rolled auth/session flow — lean on the framework. External credentials (third-party API keys, OAuth secrets) encrypted at rest, not stored plaintext.
Idioms — what a real finding looks like
# CONFIRMED — cross-tenant leak plus an unsafe upload, both attacker-reachable.
@action(detail=False, methods=["post"])
def bulk_send(self, request):
ids = request.data["invoice_ids"]
qs = Invoice.objects.filter(id__in=ids) # BUG: not tenant-scoped
# fix: qs = self.get_queryset().filter(id__in=ids) # reuse fail-closed scope
f = request.FILES["doc"]
if f.content_type == "application/pdf": # BUG: spoofable header only
default_storage.save(f.name, f) # public bucket + raw name
# fix: check f.size, whitelist the extension, sniff magic bytes, private storage
...
OWASP quick-map
The generic OWASP list only earns its keep when pinned to a real sink. Three that bite this stack, confidence-gated like everything else:
- SSRF (server-side request forgery). Any server-side URL fetch in the AI /
OCR / RAG or webhook paths must validate the target against an egress
allowlist — never fetch a user- or model-supplied URL blindly. A model that
returns a link, or a webhook payload carrying a callback URL, is attacker
reachable; a blind
requests.get(url)there can hit169.254.169.254or an internal service. Confirmed when the fetched URL flows from request/model data with no host check (seewebhook-handler,ai-integration). - Injection escape hatches. The ORM parameterizes for you, so a plain
.filter(...)is safe — flag the code that leaves it:.raw(),.extra(),RawSQL(...),cursor.execute(f"... {x}"), or any string-built query. An id or name interpolated into that string is SQL injection even though the surrounding code "uses the ORM." - Security headers. Confirm Django
SECURE_*settings — HSTS (SECURE_HSTS_SECONDS),SECURE_SSL_REDIRECT, andSecure/HttpOnly/SameSitesession and CSRF cookies — plus a Content-Security-Policy on the frontend. Missing HSTS or a permissive CSP is Likely, not Confirmed, unless you can show the header is actually absent in a response.
Adapt to your repo
Rename entreprise/Entreprise and the tenant accessor (user.entreprise_id vs
user.profile.entreprise_id) to match your project. Confirm where "logged in" is
enforced globally so you can spot endpoints that only have that and nothing more.
Know your storage default (public vs private bucket) before judging upload code.
Gotchas
- Absence is the bug: a missing tenant filter or missing permission won't appear as a red line — you must notice what isn't there. Diff-only review misses it; open the whole changed view.
- A 200 that returns another tenant's row is worse than a 500 — verify the fix
actually fails closed, don't just eyeball the filter (see
verify). - Client-supplied
entreprise/tenantids, filenames, and content-types are all attacker-controlled — never trusted, even when "the frontend sets it". - Rank by blast radius, not count. One confirmed cross-tenant leak beats ten nits.
See also
multi-tenancyrbac-permissionsai-integrationwebhook-handlermoney-decimaldb-concurrency