djust 0.4.1

StableSecurityReleased
Install
pip install djust==0.4.1

Added

  • JS Commands — client-side DOM commands chainable from templates, views, hooks, and JavaScript — Closes the single biggest DX gap vs Phoenix LiveView 1.0. Eleven commands (show, hide, toggle, add_class, remove_class, transition, dispatch, focus, set_attr, remove_attr, push) that run locally without a server round-trip, plus a push escape hatch that mixes in server events when needed. Four equivalent entry points: (1) Python helper djust.js.JS — fluent chain builder that stringifies to a JSON command list, wrapped in SafeString for safe template embedding (<button dj-click="{{ JS.show('#modal').add_class('active', to='#overlay') }}">Open</button>). (2) Client-side window.djust.js — mirror of the Python API with camelCase method names for direct JavaScript use (window.djust.js.show('#modal').addClass('active', {to: '#overlay'}).exec()). (3) Hook API — every dj-hook instance now has a this.js() method returning a chain bound to the hook element (Phoenix 1.0 parity for programmable JS Commands from hook lifecycle callbacks). (4) Attribute dispatcherdj-click (and other event-binding attributes) detect whether the attribute value is a JSON command list ([[...]]) and execute it locally; plain handler names still fire server events as before (zero breaking changes). All commands support scoped targets: to=<selector> (absolute document.querySelectorAll), inner=<selector> (scoped to origin element's descendants), closest=<selector> (walk up the DOM from origin) — a single <button dj-click="{{ JS.hide(closest='.modal') }}">Close</button> works in every modal with no per-instance IDs. The push command accepts page_loading=True to show the navigation-level loading bar while the event round-trips. Chains are immutable — every chain method returns a new JSChain, so reusing a base chain across multiple call sites never cross-contaminates. 37 new Python tests (every command + target validation + chain immutability + HTML/SafeString integration + template rendering) and 30 new JS tests (every command executing against real DOM + target resolution + chain fluency + attribute dispatcher + backwards-compat for plain event names + parseCommandValue edge cases). Zero new dependencies — the Python helper is stdlib-only and the JS interpreter is ~350 lines in a new src/26-js-commands.js module. Full guide in docs/website/guides/js-commands.md with examples for templates, hooks, chaining, and the "when to reach for what" decision tree.

  • dj-paste — paste event handling — New attribute that fires a server event when the user pastes content into a bound element (<textarea dj-paste="handle_paste">). The client extracts structured payload from the ClipboardEvent in one pass: text (clipboardData.getData('text/plain')), html (getData('text/html') for rich paste from Word/Google Docs/web pages), has_files (bool), and files (list of {name, type, size} metadata dicts for every file in clipboardData.files). When the element also carries a dj-upload="<slot>" attribute, the clipboard's FileList is routed through the existing upload pipeline — image-paste → chat, CSV-paste → table, etc. — via a new window.djust.uploads.queueClipboardFiles(element, fileList) export. Participates in the standard interaction pipeline (dj-confirm, dj-lock). By default the browser's native paste still happens so hybrid editors feel natural; add dj-paste-suppress to intercept fully (useful when routing image paste to an upload slot without dumping a data URL into a <div contenteditable>). Positional args in the attribute syntax (dj-paste="handle_paste('chat', 42)") forward via kwargs["_args"]. 11 new JS tests covering text extraction, HTML extraction, file metadata, suppress flag, missing clipboardData, double-bind protection, positional args, upload routing with and without a dj-upload slot, and graceful degradation when getData('text/html') throws. ~80 lines JS. Full guide in docs/website/guides/dj-paste.md.

  • djust_audit --ast — AST security anti-pattern scanner (#660) — Adds a new mode to djust_audit that walks the project's Python source and Django templates looking for five specific security anti-patterns, each motivated by a live vulnerability or near-miss in the 2026-04-10 a downstream consumer penetration test. Seven stable finding codes djust.X001djust.X007: X001 (ERROR) — possible IDOR: Model.objects.get(pk=...) inside a DetailView / LiveView without a sibling .filter(owner=request.user) (or user=, tenant=, organization=, team=, created_by=, author=, workspace=) scoping the queryset. X002 (WARN) — state-mutating @event_handler without any permission check (no class-level login_required/permission_required, no @permission_required/@login_required). X003 (ERROR) — SQL string formatting: .raw() / .extra() / cursor.execute() passed an f-string, a .format() call, or a "..." % ... binary-op. X004 (ERROR) — open redirect: HttpResponseRedirect(request.GET[...]) / redirect(...) without an url_has_allowed_host_and_scheme or is_safe_url guard in the enclosing function. X005 (ERROR) — unsafe mark_safe / SafeString wrapping an interpolated string (XSS risk). X006 (WARN) — template uses {{ var|safe }} (regex scan of .html files). X007 (WARN) — template uses {% autoescape off %}. Suppression via # djust: noqa X001 on the offending line, or {# djust: noqa X006 #} inside templates. New CLI flags: --ast, --ast-path <dir>, --ast-exclude <prefix> [...], --ast-no-templates. Supports --json and --strict (fail on warnings too). 52 new tests covering positive + negative cases for every checker, management-command integration, template scanning, and noqa suppression. Zero new runtime dependencies — stdlib ast + re. Full documentation in docs/guides/djust-audit.md and docs/guides/error-codes.md#ast-anti-pattern-scanner-findings-x0xx. Closes the v0.4.1 audit-enhancement batch (#657/#659/#660/#661 all shipped).

  • New consolidated djust_audit command guidedocs/guides/djust-audit.md documents all five modes of the command (default introspection, --permissions, --dump-permissions, --live, --ast), every CLI flag, CI integration examples, and exit-code conventions. Cross-linked from docs/guides/security.md.

  • Error code reference expanded with 44 new codesdocs/guides/error-codes.md now covers the A0xx static audit checks (7 codes: A001, A010, A011, A012, A014, A020, A030), the P0xx permissions-document findings (7 codes: P001–P007), and the L0xx runtime-probe findings (30 codes: L001–L091). Every code gets severity, cause, fix, and a reference to the related issue/PR.

  • {% live_input %} template tag — standalone state-bound form fields for non-Form views (#650)FormMixin.as_live_field() and WizardMixin.as_live_field() render form fields with proper CSS classes, dj-input/dj-change bindings, and framework-aware styling — but only for views backed by a Django Form class. This leaves non-form views (modals, inline panels, search boxes, settings pages, anywhere state lives directly on view attributes) without an equivalent helper. The new {% live_input %} tag fills this gap with a lightweight alternative that needs no Form class or WizardMixin. Supports 12 field types (text, textarea, select, password, email, number, url, tel, search, hidden, checkbox, radio), explicit event= override (defaults sensibly per type — textdj-input, select/radio/checkboxdj-change, hidden → none), debounce=/throttle= passthrough, framework CSS class resolution via config.get_framework_class('field_class'), HTML attribute passthrough with underscore-to-dash normalisation (aria_label="Search"aria-label="Search"), and a tested XSS escape boundary via a new shared djust._html.build_tag() helper. Example: {% live_input "text" handler="search" value=query debounce="300" placeholder="Search..." %}. 56 new tests including an explicit XSS matrix across every field type and attribute. See docs/guides/live-input.md for the full setup guide.

  • djust_audit --live <url> — runtime security-header and CSWSH probe (#661) — Adds a new mode to djust_audit that fetches a running deployment with stdlib urllib and validates security headers (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, COOP, CORP), cookies (HttpOnly, Secure, SameSite on session/CSRF cookies), information-disclosure paths (/.git/config, /.env, /__debug__/, /robots.txt, /.well-known/security.txt), and optionally probes the WebSocket endpoint with Origin: https://evil.example to verify the CSWSH defense from #653 is actually enforced end-to-end. This catches the class of production issues where the setting is correctly configured in settings.py but the response is stripped, rewritten, or never emitted by the time it reaches the client — a downstream consumer pentest caught a critical Content-Security-Policy missing case this way (django-csp was configured but the header was absent from production responses, stripped by an nginx ingress). 30 new stable finding codes djust.L001djust.L091 cover every check class so CI configs can suppress specific codes by number. New CLI flags: --live <url>, --paths (multi-URL), --no-websocket-probe, --header 'Name: Value' (for staging auth), --skip-path-probes (for WAF-protected environments). Supports --json and --strict (fail on warnings too). Zero new runtime dependencies — stdlib urllib for HTTP, optional websockets package for the WebSocket probe (skipped with an INFO finding if not installed).

    See docs/guides/djust-audit.md.

  • New static security checks in djust_check / djust_audit (#659) — Seven new check IDs fire from check_configuration when Django runs python manage.py check: A001 (ERROR) — WebSocket router not wrapped in AllowedHostsOriginValidator (static-analysis companion to #653 for existing apps built from older scaffolds). A010 (ERROR) — ALLOWED_HOSTS = ["*"] in production. A011 (ERROR) — ALLOWED_HOSTS mixes "*" with explicit hosts (the wildcard makes the explicit entries meaningless). A012 (ERROR) — USE_X_FORWARDED_HOST=True combined with wildcard ALLOWED_HOSTS enables Host header injection. A014 (ERROR) — SECRET_KEY starts with django-insecure- in production (scaffold default not overridden before deployment). A020 (WARNING) — LOGIN_REDIRECT_URL is a single hardcoded path but the project has multiple auth groups/permissions (catches the "every role lands on the same dashboard" anti-pattern). A030 (WARNING) — django.contrib.admin installed without a known brute-force protection package (django-axes, django-defender, etc.). Each check has essentially zero false-positive risk, has a fix_hint pointing at the remediation, and was motivated by the 2026-04-10 a downstream consumer pentest report. Out of scope for this PR: manifest scanning (k8s/helm/docker-compose env blocks) — deferred to a follow-up. Python-level settings.py values cover the common case.

  • djust_audit --permissions permissions.yaml — declarative permissions document for CI-level RBAC drift detection (#657) — Adds a new flag to djust_audit that validates every LiveView against a committed, human-readable YAML document describing the expected auth configuration for each view. CI fails on any deviation (view declared public but has auth in code, permission list mismatch, undeclared view in strict mode, stale declaration, etc.). This closes a structural gap the existing audit couldn't catch: djust_audit today can tell "no auth" from "some auth", but not that login_required=True should have been permission_required=['claims.view_supervisor']. The permissions document IS the ground truth. Seven stable error codes (djust.P001 through djust.P007) cover every deviation class. Also adds --dump-permissions to bootstrap a starter YAML from existing code, and --strict to fail CI on any finding. Full documentation in docs/guides/permissions-document.md. Motivated by a downstream consumer pentest finding 10/11 where every view had login_required=True set and djust_audit reported them all as protected, but the lowest-privilege authenticated user could ID-walk the entire database.

  • WizardMixin for multi-step LiveView form wizards — General-purpose mixin managing step navigation, per-step validation, and data collection for guided form flows. Provides next_step, prev_step, go_to_step, update_step_field, validate_field, and submit_wizard event handlers. Template context includes step indicators, progress, form data/errors, and pre-rendered field HTML via as_live_field(). Re-validates all steps on submission to guard against tampered WebSocket replays. (#632)

    See docs/website/guides/wizards.md.

Security

  • LOW: Nonce-based CSP support — drop 'unsafe-inline' from script-src / style-src — djust's inline <script> and <style> emissions (handler metadata bootstrap in TemplateMixin._inject_handler_metadata, live_session route map in routing.get_route_map_script, and the PWA template tags djust_sw_register, djust_offline_indicator, djust_offline_styles) now read request.csp_nonce when available (set by django-csp when CSP_INCLUDE_NONCE_IN covers the relevant directive) and emit a nonce="..." attribute on the tag. When no nonce is available (django-csp not installed, or CSP_INCLUDE_NONCE_IN not set), the tags emit without a nonce attribute — fully backward compatible with apps still allowing 'unsafe-inline'. Apps that want strict CSP can now set CSP_INCLUDE_NONCE_IN = ("script-src", "script-src-elem", "style-src", "style-src-elem") in settings.py, drop 'unsafe-inline' from CSP_SCRIPT_SRC / CSP_STYLE_SRC, and get strict CSP XSS protection across all djust-generated inline content. The PWA tags djust_sw_register, djust_offline_indicator, and djust_offline_styles now use takes_context=True to read the request from the template context — they still work with the same template syntax ({% djust_sw_register %} etc.) as long as a RequestContext is used (Django's default for template rendering). See docs/guides/security.md for the full setup. Reported via external penetration test 2026-04-10 (FINDING-W06). Closes the v0.4.1 security hardening batch (#653 / #654 / #655). (#655)

  • MEDIUM: Gate VDOM patch timing/performance metadata behind DEBUG / DJUST_EXPOSE_TIMINGLiveViewConsumer previously attached timing (handler/render/total ms) and performance (full nested timing tree with handler and phase names) to every VDOM patch response unconditionally, regardless of settings.DEBUG. Combined with CSWSH (#653) this let cross-origin attackers observe server-side code-path timings, enabling timing-based code-path differentiation (DB hit vs cache miss, valid vs invalid CSRF), internal handler/phase name disclosure, and load-based DoS scheduling. Now gated on a new helper _should_expose_timing() which returns True only when settings.DEBUG or the new settings.DJUST_EXPOSE_TIMING is True. Upgrade notes: production behavior change — existing clients that consumed response.timing / response.performance in production will no longer see those fields; opt in via DJUST_EXPOSE_TIMING = True in settings for staging/profiling. The browser debug panel is unaffected (it receives timing via the existing _attach_debug_payload path, which is already gated on DEBUG). Reported via external penetration test 2026-04-10. References: CWE-203, CWE-215, OWASP A09:2021. (#654)

  • HIGH: Validate WebSocket Origin header to prevent Cross-Site WebSocket Hijacking (CSWSH)LiveViewConsumer.connect() previously accepted the WebSocket handshake without validating the Origin header, and DjustMiddlewareStack did not wrap the router in an origin validator. A cross-origin attacker could mount any LiveView and dispatch any event from a victim's browser. Now the consumer rejects disallowed origins with close code 4403 before accepting the handshake, and DjustMiddlewareStack wraps its inner application in channels.security.websocket.AllowedHostsOriginValidator by default (defense in depth). Missing Origin is still allowed so non-browser clients (curl, test WebsocketCommunicator) continue to work. Upgrade notes: ensure settings.ALLOWED_HOSTS does NOT contain * in production; if you need to opt out for a specific stack, use DjustMiddlewareStack(inner, validate_origin=False) (not recommended). Reported via external penetration test 2026-04-10. (#653)

  • Enforce login_required on HTTP GET path — Views with login_required = True rendered full HTML to unauthenticated users on the initial HTTP GET. The WebSocket connection was correctly rejected, but the pre-rendered page content was already visible. Now calls check_view_auth() before mount() on HTTP GET and returns 302 to LOGIN_URL. Also calls handle_params() after mount() on HTTP GET to match the WebSocket path's behavior, preventing state flash on URL-param-dependent views. (#636, fixes #633, #634)

Fixed

  • Prevent SynchronousOnlyOperation in PerformanceTracker.track_context_size — The tracker called sys.getsizeof(str(context)), which triggered QuerySet.__repr__() on any unevaluated querysets in the context dict. __repr__ calls list(self[:21]), evaluating the queryset against the database — raising SynchronousOnlyOperation in the async WebSocket path. Now uses a shallow per-value getsizeof sum that does not invoke __repr__/__str__ on values, so lazy objects stay lazy. Size estimates are now slightly less precise (don't include recursive inner size) but safe in async contexts. (#651, fixes #649)

  • Apply RemoveChild patches before batched InsertChild in same parent groupapplyPatches in client.js:1379-1440 was filtering InsertChild patches out of each parent group and applying them via DocumentFragment before iterating the group for the RemoveChild patches in that same parent, violating the top-level Remove → Insert phase order. This was latent for keyed content (monotonic dj-ids meant removes still found targets by ID), but fired for <!--dj-if--> placeholder comments — they have no dj-id (only elements get IDs), so their removes fall back to index-based lookup, and by the time the removes ran, the batched inserts had already prepended the new content and shifted indices. The removes then deleted the just-inserted content, leaving empty tab content on multi-tab views (symptom: a downstream consumer tab switches showing blank content after the first switch). Fix: split each parent group into non-Insert vs Insert lists, apply all non-Insert patches first in their phase-sorted order, then batch the inserts. (#643, fixes #641, closes #642)

  • dj-patch on <a> tags uses href when attribute value is empty — Boolean dj-patch on anchor elements (<a href="?tab=docs" dj-patch>) was resolving to the current URL instead of the href destination. Now falls back to el.getAttribute('href') when dj-patch is empty and the element is <a>. (#640)

  • Normalize Model instances in render_full_template before passing to Rust — Django FK fields are class-level descriptors not present in __dict__. Rust's FromPyObject extracts __dict__ which has claimant_id=1 (raw FK int) instead of the related object. Now always calls normalize_django_value() on pre-serialized context so FK relationships are resolved via getattr() and traversable with dot notation ({{ claim.claimant.first_name }}). (#639)

  • Render Django Form/BoundField to SafeString HTML in template context{{ form.field_name }} rendered as empty string because the Rust renderer extracted Form.__dict__ which doesn't contain computed BoundField attributes. Now pre-renders Form and BoundField objects to SafeString HTML via widget.render() in all four code paths (serialization, template serialization, template rendering, and LiveView state sync). (#631, fixes #621)

  • Correct has_ids attribute name in WebSocket mount responsewebsocket.py checked for "data-dj-id=" but the Rust renderer emits "dj-id=" attributes. This caused _stampDjIds() to be skipped on pre-rendered pages, breaking VDOM patches for large content swaps (e.g. tab switching) while small patches still worked. The SSE path already had the correct check. (#630, fixes #629)

  • Sync input .value from attribute after innerHTML/VDOM patch — When navigating backward in a multi-step wizard, text input values were not visually restored even though the server sent correct VDOM patches. setAttribute('value', x) only updates the HTML attribute (defaultValue), not the .value DOM property. Now syncs .value from the attribute in preserveFormValues(), broadcast patches, and morphElement(). Skips focused inputs, checkboxes, radios, and file inputs. (#625, fixes #624)

All releases · Atom feed