Sikkerhet: tett path traversal i post-id og herd innlogging/upload

Funn fra sikkerhetsgjennomgang:
- HØY: post_id ble ikke validert, så en gjest kunne nå andres innlegg
  via gjest/../../posts/<slug> (lese utkast, laste opp/slette bilder,
  publisere eierens utkast). Slug og gjestenavn valideres nå mot samme
  alfabet de lages med, og stien sjekkes etter oppslag.
- Bilder: pikselantall sjekkes før dekoding (maks 40 MP), og
  DecompressionBombError håndteres.
- Innlogging: CSRF-token kreves også på login (login-CSRF), og
  innloggingsbrems etter 5 feil per brukernavn.
- Passordbytte ugyldiggjør alle andre sesjoner (stempel i cookien).
- Content-Security-Policy og X-Frame-Options på alle sider; admin har
  ingen inline script lenger (admin.js).
- Gjestenavn og eier-slugs kan ikke lenger kollidere på /navn/.
- Healthcheck respekterer SOAPBOX_BASE_PATH; login-redirect beholder
  base path; run.sh dropper capabilities og setter minnegrense; uv
  pinnet til 0.11.

Rettet også en reell feil: existing_slugs() listet feil katalog
(Path /  er identitet), så slug-unikhet virket ikke.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcEy43fNYpwg6K6oTKakWR
This commit is contained in:
Ole-Morten Duesund 2026-08-26 14:07:14 +02:00
commit d47558c6c1
14 changed files with 284 additions and 31 deletions

View file

@ -114,15 +114,14 @@ def test_guest_flow_and_isolation(client, cfg):
def test_login_next_no_open_redirect(client):
def attempt(nxt):
client.get("/admin/login") # ferskt CSRF-token (roteres ved innlogging)
creds = {"username": "eier", "password": "hemmelig123", "csrf": csrf(client)}
return client.post("/admin/login", data={**creds, "next": nxt})
for bad in ("//evil.com", "/\\evil.com", "https://evil.com"):
r = client.post(
"/admin/login", data={"username": "eier", "password": "hemmelig123", "next": bad}
)
assert r.headers["Location"] == "/admin/", bad
r = client.post(
"/admin/login", data={"username": "eier", "password": "hemmelig123", "next": "/admin/users"}
)
assert r.headers["Location"] == "/admin/users"
assert attempt(bad).headers["Location"] == "/admin/", bad
assert attempt("/admin/users").headers["Location"] == "/admin/users"
def test_csrf_enforced(client):
@ -233,3 +232,18 @@ def test_rebuild_on_startup(cfg):
(cfg.public_dir / "index.html").write_text("GAMMEL")
create_app(cfg)
assert "GAMMEL" not in (cfg.public_dir / "index.html").read_text()
def test_slug_uniqueness(client, cfg):
login(client)
token = csrf(client)
ids = []
for _ in range(2):
r = client.post("/admin/new", data={"csrf": token})
pid = r.headers["Location"].split("/admin/edit/")[1]
r = client.post(
f"/admin/edit/{pid}",
data={"csrf": token, "title": "Samme", "body": "x", "action": "publish"},
)
ids.append(r.headers["Location"].split("/admin/edit/")[1])
assert ids == ["_owner/samme", "_owner/samme-2"]