Home Features Docs Blog Security Examples FAQ
Security - Part 2 of 2

WebSocket Event Security: Three Layers of Protection in djust

djust Team | | 4 min read
Layered shield diagram representing three levels of WebSocket event security

WebSocket connections are long-lived and bidirectional. That makes them powerful for real-time applications, but it also means a single compromised connection can send an unbounded stream of events to your server. In djust's LiveView architecture, every event from the browser calls a method on your Python class. Without proper guards, an attacker could invoke internal methods, overwhelm your server, or trigger operations you never intended to expose.

We've added three layers of defense to djust's WebSocket event dispatch. Each layer addresses a different threat, and together they provide defense in depth for your LiveView handlers.

Layer 1: Event Name Guard

Before djust even looks up a method on your LiveView class, the event name itself is validated. This is a fast syntactic filter that operates at the dispatch level, rejecting malformed names before any further processing occurs.

The guard enforces a strict regex pattern: event names must consist of alphanumeric characters, underscores, and hyphens only. This rejects:

  • Names starting with _ (private methods) or __ (dunder methods)
  • Names containing dots, dashes at the start, spaces, or other special characters
  • Empty strings and names starting with a number

This means getattr(self, event_name) is never called with a name like __class__, .hidden, or an empty string. The connection receives an error response and the attempt is logged. This layer is intentionally lightweight — it is not the primary security mechanism, but rather a fast first pass that eliminates obviously invalid event names before reaching the main allowlist check.

Layer 2: The @event Decorator Allowlist

The name guard filters out malformed event names. The @event decorator does the real security work: it tells djust which methods are explicitly allowed to be called from the client.

from djust.live import LiveView, event

class CounterView(LiveView):
    template_name = "counter.html"
    count = 0

    @event
    def increment(self):
        self.count += 1

    @event
    def decrement(self):
        self.count -= 1

    def _recalculate_total(self):
        # Not decorated - never callable from client
        self.total = self.count * self.multiplier

The behavior of undecorated methods depends on the event_security config setting, which has three modes:

# settings.py
DJUST = {
    "event_security": "strict",  # "open", "warn", or "strict" (default)
}
  • open - All public methods are callable (legacy behavior, not recommended)
  • warn - Undecorated handlers work but log a deprecation warning (useful during migration from older versions)
  • strict - Only @event-decorated methods or those listed in _allowed_events are callable (default)

If you have methods that need to be event handlers but cannot use the decorator for some reason, add them to the _allowed_events set on your view class:

class MyView(LiveView):
    _allowed_events = {"legacy_handler", "another_handler"}

Since strict is the default, new projects are secure out of the box. If you are upgrading from an older version of djust, you can temporarily set event_security to "warn" to identify undecorated handlers via log warnings, then switch back to the default once all handlers have the @event decorator.

Layer 3: Server-Side Rate Limiting

Even with the first two layers, a valid event can be called thousands of times per second. The third layer applies rate limiting directly on the WebSocket connection using a token bucket algorithm.

Global rate limiting applies to all events on a connection:

# settings.py
DJUST = {
    "ws_rate_limit": {
        "rate": 30,            # Events per second
        "burst": 50,           # Max burst allowance
        "max_warnings": 3,     # Warnings before disconnect
        "max_message_size": 65536,  # 64KB max payload
    },
}

When a connection exceeds the rate, it receives a warning. After max_warnings violations, the server disconnects the client with WebSocket close code 4429 (a custom code mirroring HTTP 429 Too Many Requests).

For expensive operations like database writes or external API calls, you can set tighter limits per handler with the @rate_limit decorator:

from djust.live import LiveView, event, rate_limit

class SearchView(LiveView):
    template_name = "search.html"

    @event
    def update_filter(self):
        # Default rate limit applies
        self.apply_filters()

    @event
    @rate_limit(rate=2, burst=5)
    def execute_search(self):
        # Expensive DB query - max 2 per second
        self.results = SearchIndex.query(self.query)

    @event
    @rate_limit(rate=1, burst=1)
    def export_results(self):
        # Very expensive - max 1 per second, no burst
        self.download_url = generate_export(self.results)

The message size limit is also enforced at this layer. Any WebSocket frame exceeding max_message_size (64KB by default) is rejected before parsing.

How the Layers Work Together

When a WebSocket event arrives, it passes through all three layers in order:

  1. Rate limiter checks if the connection has exceeded its event budget or message size limit
  2. Name guard validates the event name format (regex pattern)
  3. Allowlist verifies the method is decorated with @event or listed in _allowed_events

Only after passing all three checks does djust call the handler method. Each layer is independent and can reject the event with a specific error, making debugging straightforward while keeping your application locked down.

Migration Guide

If you're upgrading from an earlier version of djust:

  1. Update djust to the latest version
  2. Set event_security to "warn" in your settings temporarily to avoid breaking existing handlers
  3. Add @event decorators to all your client-callable handler methods
  4. Check your logs for any remaining deprecation warnings about undecorated handlers
  5. Remove the "warn" override from your settings — strict is the default and will take effect automatically

The defaults are designed to work for most applications out of the box. You should only need to adjust rate limits if you have handlers that are called at high frequency (like real-time collaboration) or handlers that are particularly expensive.

Security works best when it's invisible to developers who follow best practices and impossible to bypass for those who don't. These three layers bring that philosophy to djust's WebSocket event handling.

Share this post

Related Posts