Back to skill

Security audit

Django

Security checks for vulnerabilities and agentic risk

Overview

This is a Django help skill with scoped local preference memory; I found no hidden execution, data export, or deceptive behavior.

Before installing, review the local memory behavior: this skill can use and update ~/Clawic/data/django/ and may consult ~/Clawic/profile.yaml for general preferences. Do not store secrets, credentials, sensitive production details, or untrusted quoted instructions in those files. Also treat the custom authentication backend and admin HTML notes as guidance to review carefully rather than code to copy verbatim.

Vulnerability Patterns
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Warning
Location
auth.md:58
Finding
Timing-Based Account Enumeration in Custom Authentication Backend<![CDATA[ ## Vulnerability Details **File Location**: `auth.md:58-69` **Vulnerability Type**: Authentication timing side channel **Risk Level**: Medium ### Vulnerable Code ```python class EmailBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): user = User.objects.filter(email__iexact=username).first() if user and user.check_password(password) and self.user_can_authenticate(user): return user return None ``` The guidance immediately following the example recognizes the missing protection: ```text Run `check_password` even when the user is absent, or response timing tells an attacker which emails exist. Django's own `ModelBackend` calls `UserModel().set_password(password)` for exactly this reason. ``` ### Technical Analysis The conditional uses Python short-circuit evaluation. If no matching user exists, `user` is false and `user.check_password(password)` is never executed. For an existing account, Django performs an intentionally expensive password-hashing operation. Consequently, authentication attempts for nonexistent accounts are generally faster than attempts for existing accounts with incorrect passwords. Repeated measurements can reduce ordinary network noise and allow an attacker to statistically distinguish registered email addresses. The example contradicts its own subsequent recommendation and may be copied directly into a production application. The issue does not reveal passwords or grant authentication by itself, but it exposes an account-existence oracle. ### Attack Path 1. An attacker obtains or generates a list of candidate email addresses. 2. The attacker submits repeated login attempts using the same invalid password for each candidate. 3. Requests for nonexistent users return without running the password hasher. 4. Requests for existing users invoke `check_password()` and exhibit a measurably higher processing time. 5. The attacker aggregates multi ...[truncated 623 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Replace the short-circuit implementation with one that always performs a password-hashing operation: ```python class EmailBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): UserModel = get_user_model() try: user = UserModel.objects.get(email__iexact=username) except UserModel.DoesNotExist: # Consume approximately the same hashing cost as a real password check. UserModel().set_password(password) return None if user.check_password(password) and self.user_can_authenticate(user): return user return None ``` Additional hardening measures: 1. Add request-level or proxy-level login throttling. 2. Return identical response bodies and status codes for unknown users and incorrect passwords. 3. Add automated tests that verify both paths execute a password-hashing operation. 4. Avoid strict single-request timing assertions because timing is noisy; instead, test the control flow through mocked hasher calls. 5. Monitor distributed authentication failures without logging submitted credentials. ]]>

T02 · Agent Memory Poisoning

Note
Location
setup.md:11
Finding
Persistent Cross-Session Preference Storage Creates a Memory-Poisoning Surface<![CDATA[ ## Vulnerability Details **File Location**: `setup.md:11-28` **Vulnerability Type**: Persistent Agent state poisoning **Risk Level**: Low ### Vulnerable Instructions ```text 1. Read `~/Clawic/data/django/config.yaml` if it exists. Apply its values. 2. For anything absent, use the defaults in the Configuration table of `SKILL.md` — do not ask. - `django_version: 5.2`, `database: postgres`, `api_layer: drf`, `settings_layout: split-by-env`, `project_layout: flat`, `task_queue: celery`, `test_runner: django`, `deploy_target: gunicorn-wsgi`, `destructive_confirm: true`. 3. Read `~/Clawic/data/django/memory.md` for prior context (their project shape, recurring pain points). Absence is fine; proceed without comment. 4. Universal values (units, locale, timezone) fall back to `~/Clawic/profile.yaml` when this skill has no key of its own. ``` ```text Write to config or memory **only** when the user states a preference in the course of the work — never as a preflight questionnaire. - User names a Django version, database engine, API layer, settings layout, project layout, task queue, test runner, or deployment target → update the matching key in `~/Clawic/data/django/config.yaml`. - User expresses a habit or stance (fat models vs services, whether migrations may be applied directly, banned packages, how much explanation they want with generated code) → record it under the relevant preference area (tooling, thresholds, conventions, platform, risk posture, output format, work order, integrations, restrictions, cadence) in `~/Clawic/data/django/memory.md`. - User corrects earlier guidance → update the stored value so it is not repeated. ``` ### Technical Analysis The Skill automatically reads persistent files and applies their contents to later sessions. It also directs the Agent to convert conversational statements into persistent configuration or free-form Markdown memory. This behavior is disclosed and limited to user preferences and project context ...[truncated 2233 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require explicit user confirmation immediately before writing any persistent preference. 2. Store only structured, allowlisted keys with strict type and value validation. 3. Treat `memory.md` as untrusted data and never interpret its contents as executable instructions or authority to override current user requests and safety controls. 4. Distinguish direct user statements from repository content, logs, pasted documents, quoted messages, and third-party instructions. 5. Record provenance, timestamp, and the exact confirmed value for every stored preference. 6. Provide a clear command to display, edit, and delete all stored Django memory. 7. Never store credentials, secrets, personal data, raw incident payloads, or sensitive production details. 8. Limit file permissions to the owning user and reject symlinks or unexpected file types when reading and writing state. 9. Avoid reading `~/Clawic/profile.yaml` unless the current task requires a specific global preference and the user has authorized that access. 10. Revalidate high-impact preferences, such as permission to execute migrations or use raw SQL, in the current session before taking action. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (7)

Credential Access

High
Category
Privilege Escalation
Content
- Two levels: `has_permission(request, view)` runs before the handler; `has_object_permission(request, view, obj)` runs only when `get_object()` is called — so a custom list endpoint that never calls it gets no object checks at all.
- `SAFE_METHODS` is the idiom for read-only-for-others permissions. Combine classes with `&`, `|`, `~`.
- Authentication answers "who"; permission answers "may they". A 401 means no credentials were recognized, a 403 means they were and it is not enough — returning the wrong one sends clients into a refresh loop.
- JWT is a dependency, not part of DRF. Its tradeoff is revocation: a stateless token stays valid until it expires, so keep access tokens short and put logout on the refresh token.
- CORS is not authentication and not part of DRF either; a browser preflight failing looks like an auth bug and is a middleware configuration.

## Pagination, Filtering, Versioning
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Intent-Code Divergence

Medium
Confidence
99% confidence
Finding
The statement claims that HTML returned from a `list_display` callable is not escaped and therefore should use `format_html`. In Django admin, plain strings returned by display methods are escaped by default; `format_html` is used when you intentionally want to safely construct HTML markup. This incorrect guidance can cause developers to misunderstand the escaping model and, depending on how they act on it, either introduce unsafe uses of `mark_safe`/raw HTML or make incorrect security decisions about XSS handling in admin pages.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
| `migrate --fake` | Writes a row in `django_migrations`, changes no schema | Correct only when the schema already matches; otherwise recorded history becomes a lie |
| `migrate --fake-initial` | Fakes only the initial migration if the tables exist | The safe variant for adopting an existing database |
| `migrate app 0006` | Migrates *backwards* to 0006 | Runs reverse operations; irreversible ones raise mid-way |
| `flush` | Deletes all data, keeps the schema, re-runs post-migrate | No confirmation with `--noinput` |
| `sqlflush` | Prints the truncation SQL | Harmless to print, catastrophic to pipe |
| `loaddata` | Loads a fixture, calling `save()` per object | Signals fire; existing rows with the same pk are overwritten |
| `dumpdata` | Serializes via `_default_manager` | A filtered default manager silently omits rows |
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The setup instructions tell the agent to read user-local files such as ~/Clawic/data/django/config.yaml, ~/Clawic/data/django/memory.md, and ~/Clawic/profile.yaml automatically on first use, without any user-facing disclosure or consent step in the skill itself. This creates a privacy and boundary-risk issue because the skill is authorized to ingest potentially sensitive local context unrelated to the immediate task, and the instruction to 'Do not interview the user' reduces transparency rather than narrowing access.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The skill directs persistent writes to local config and memory files based on conversational content, again without an explicit warning that user data will be modified. Persistent state changes can silently record preferences, constraints, or project details, creating privacy, integrity, and surprise-modification risks, especially because the skill encourages automatic updating of stored values when the user mentions or corrects preferences.

Missing User Warnings

Low
Confidence
91% confidence
Finding
Line L03 directs the user to create `~/Clawic/data/django/memory.md`, which is a file-writing action affecting user data. The skill description does not include any warning or disclosure that it will create or modify a file on the local system.

Missing User Warnings

Low
Confidence
80% confidence
Finding
This is a markdown file, so SQP-2 applies to omitted warnings about behaviors affecting user data or privacy. Line L097 mentions that unhandled exceptions emailed by AdminEmailHandler include request data, but frames it as implementation guidance rather than an explicit user-facing warning to avoid leaking sensitive request contents.

Static analysis

No suspicious patterns detected.