Files
shelfmark/tests/core/test_socketio_origins.py
T
splitsec2 61c7ad3c62 Merge commit from fork
* fix(torrent): check each redirect hop when fetching a torrent file

The torrent file fetch followed redirects by hand without looking at where a
hop pointed, so a download link could make the server request a loopback,
link-local or LAN address. Each hop (the initial URL and every redirect
Location) must now be http or https and resolve only to global addresses.
The configured Prowlarr/Newznab origin is exempt, as it usually lives on the
LAN. The API key still only goes to that origin.

* fix(socketio): only accept websocket handshakes from the app's own origin

The Socket.IO server allowed every origin. It now uses the same-origin check
python-engineio applies by default (Host, or X-Forwarded-Proto and
X-Forwarded-Host behind a proxy). SOCKETIO_ALLOWED_ORIGINS takes a comma
separated list of extra origins for proxies that change Host without
forwarding it.

* fix(custom-script): do not pass a file name that starts with a dash as the first argument

In relative path mode the script got the organized file's relative path as
its first argument, which many scripts read as an option when the name starts
with a dash. Such a name is now passed with a ./ prefix. Absolute mode is
unchanged.
2026-10-21 01:34:38 -04:00

76 lines
2.2 KiB
Python

"""Socket.IO handshakes are limited to the app's own origin (plus configured extras)."""
from __future__ import annotations
from flask import Flask
from flask_socketio import SocketIO
from shelfmark.core.socketio_origins import build_cors_allowed_origins
_HANDSHAKE = "/socket.io/?EIO=4&transport=polling"
def _client(extra: list[str] | None = None):
app = Flask(__name__)
SocketIO(
app,
async_mode="threading",
cors_allowed_origins=build_cors_allowed_origins(extra or []),
)
return app.test_client()
def _handshake(client, origin: str | None, **headers: str) -> int:
request_headers = dict(headers)
if origin is not None:
request_headers["Origin"] = origin
return client.get(
_HANDSHAKE, headers=request_headers, base_url="http://shelf.example"
).status_code
def test_same_origin_handshake_is_accepted():
assert _handshake(_client(), "http://shelf.example") == 200
def test_foreign_origin_handshake_is_refused():
assert _handshake(_client(), "https://evil.example") == 400
def test_sibling_subdomain_is_refused():
assert _handshake(_client(), "http://other.example") == 400
def test_non_browser_client_without_origin_is_accepted():
assert _handshake(_client(), None) == 200
def test_forwarded_proto_and_host_origin_is_accepted():
status = _handshake(
_client(),
"https://books.example.org",
**{"X-Forwarded-Proto": "https", "X-Forwarded-Host": "books.example.org"},
)
assert status == 200
def test_https_origin_without_forwarded_headers_is_refused_by_default():
assert _handshake(_client(), "https://shelf.example") == 400
def test_extra_origin_is_accepted_and_others_still_refused():
client = _client(["https://shelf.example"])
assert _handshake(client, "https://shelf.example") == 200
assert _handshake(client, "http://shelf.example") == 200
assert _handshake(client, "https://evil.example") == 400
def test_default_is_the_library_same_origin_check():
assert build_cors_allowed_origins([]) is None
def test_app_does_not_allow_any_origin():
from shelfmark import main as main_module
assert main_module.socketio.server.eio.cors_allowed_origins is None