mirror of
https://github.com/Donkie/Spoolman.git
synced 2026-10-21 17:42:16 -05:00
Tasks 1-3 of the backend security audit all need to answer "is this browser
origin someone we trust". Build it once, driven entirely by the existing
SPOOLMAN_CORS_ORIGIN rather than a new env var.
- Unset means same-origin only, which is right for nearly every deployment.
Non-browser clients (Moonraker, OctoPrint) send no Origin and are unaffected.
- A list means those origins plus our own.
- "*" and debug mode mean "I do not want origin checks" and are honoured as
such, with a loud startup warning. An operator who writes "*" is opting out;
reinterpreting it as something narrower would leave them no way to say so.
Also accept X-Forwarded-Host, so a reverse proxy that rewrites Host (nginx
without proxy_set_header Host $host, Apache without ProxyPreserveHost On) does
not see the guard reject its own web UI. A malicious page cannot set that
header on any of the vectors the guard exists to stop: forms cannot set request
headers, nor can the browser WebSocket API, and a fetch that adds one needs a
CORS preflight it will not get.
get_cors_origin() now normalizes its entries -- it was a bare split(","), so
"https://a, https://b" yielded " https://b", which no Origin header can match.
CORS itself benefits from the same fix. The raw value is kept in the startup
log so a typo stays visible.
Adds tests/ for backend unit tests, runnable with `poe test` and wired into the
lefthook ci group. Nothing enforces this helper yet; that is tasks 1-3.
49 lines
1.8 KiB
Python
49 lines
1.8 KiB
Python
"""Tests for environment variable parsing."""
|
|
|
|
import pytest
|
|
|
|
from spoolman import env
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("raw", "expected"),
|
|
[
|
|
("https://spoolman.local", "https://spoolman.local"),
|
|
(" https://spoolman.local ", "https://spoolman.local"),
|
|
("https://spoolman.local/", "https://spoolman.local"),
|
|
("https://spoolman.local///", "https://spoolman.local"),
|
|
("HTTPS://Spoolman.Local", "https://spoolman.local"),
|
|
("*", "*"),
|
|
],
|
|
)
|
|
def test_normalize_origin(raw: str, expected: str):
|
|
assert env.normalize_origin(raw) == expected
|
|
|
|
|
|
def test_get_cors_origin_unset(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.delenv("SPOOLMAN_CORS_ORIGIN", raising=False)
|
|
assert env.get_cors_origin() is None
|
|
assert env.is_cors_defined() is False
|
|
|
|
|
|
def test_get_cors_origin_single(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setenv("SPOOLMAN_CORS_ORIGIN", "https://spoolman.local")
|
|
assert env.get_cors_origin() == ["https://spoolman.local"]
|
|
assert env.is_cors_defined() is True
|
|
|
|
|
|
def test_get_cors_origin_trims_list_entries(monkeypatch: pytest.MonkeyPatch):
|
|
"""A space after the comma must not produce an entry no Origin header can ever match."""
|
|
monkeypatch.setenv("SPOOLMAN_CORS_ORIGIN", "https://a.local, https://b.local/")
|
|
assert env.get_cors_origin() == ["https://a.local", "https://b.local"]
|
|
|
|
|
|
def test_get_cors_origin_drops_empty_and_duplicate_entries(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setenv("SPOOLMAN_CORS_ORIGIN", "https://a.local,,https://a.local/, ")
|
|
assert env.get_cors_origin() == ["https://a.local"]
|
|
|
|
|
|
def test_get_cors_origin_raw_is_unparsed(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setenv("SPOOLMAN_CORS_ORIGIN", " https://a.local, https://b.local ")
|
|
assert env.get_cors_origin_raw() == " https://a.local, https://b.local "
|