Answer only to the host names of Bindery

Without a login, a web page that the user visits could reach Bindery
under a name of its own that points at the address of Bindery, and read
and change everything. The "none" mode now answers only to addresses
and to names of a private network. ALLOWED_HOSTS sets the names for
each mode.
This commit is contained in:
tim
2026-10-02 23:37:33 +02:00
parent 2d9796d77c
commit 751b52da77
7 changed files with 113 additions and 0 deletions
+46
View File
@@ -10,6 +10,7 @@ import pytest
from fake_cwa import FakeCwa
from itsdangerous import TimestampSigner
from test_build import PNG
from werkzeug.security import generate_password_hash
from bindery import auth, config, orchestrator, web
from bindery.store import Store
@@ -242,6 +243,51 @@ def test_incomplete_login_settings_refuse_every_request(client, monkeypatch, set
assert client.get("/").status_code == 200
@pytest.mark.parametrize(
("mode", "allowed", "host", "accepted"),
[
# Without a login, only an address or a name of a private network.
("none", [], "localhost", True),
("none", [], "127.0.0.1:8085", True),
("none", [], "192.168.1.5:8085", True),
("none", [], "[fd00::5]:8085", True),
("none", [], "bindery:8085", True),
("none", [], "nas.local", True),
("none", [], "NAS.Home.Arpa.", True),
("none", [], "bindery.example.com", False),
("none", [], "rebind.attacker.example:8085", False),
("none", [], "notlocal", True),
("none", [], "evil.com.local.attacker.example", False),
# With a login, each name.
("local", [], "bindery.example.com", True),
("proxy", [], "bindery.example.com", True),
# A list counts in each mode.
("none", ["bindery.example.com"], "bindery.example.com:443", True),
("none", ["bindery.example.com"], "localhost", False),
("none", ["bindery.example.com"], "x.bindery.example.com", False),
("local", [".example.com"], "bindery.example.com", True),
("local", [".example.com"], "example.com", True),
("local", [".example.com"], "badexample.com", False),
("local", ["example.com"], "attacker.example", False),
("none", ["*"], "anything.example", True),
],
)
def test_bindery_answers_only_to_its_host_names(client, monkeypatch, mode, allowed, host, accepted):
monkeypatch.setattr(config, "AUTH_METHOD", mode)
monkeypatch.setattr(config, "LOCAL_USERNAME", "reader")
monkeypatch.setattr(config, "LOCAL_PASSWORD_HASH", generate_password_hash("pw", method="pbkdf2:sha256:1000"))
monkeypatch.setattr(config, "ALLOWED_HOSTS", allowed)
for path in ("/", "/api/auth/check", "/static/style.css"):
response = client.get(path, headers={"Host": host})
assert response.status_code == (200 if accepted else 400), path
if not accepted:
assert response.json == {
"error": f"Bindery does not answer to the host name {host}. Add it to ALLOWED_HOSTS."
}
# The health check of the container works under each name.
assert client.get("/api/health", headers={"Host": host}).status_code == 200
def test_api_key(client, cwa, monkeypatch):
monkeypatch.setattr(config, "API_KEY", "k" * 32)
assert client.get("/api/stories", headers={"X-Api-Key": "wrong"}).status_code == 401