"""Regresjonstester for funn fra sikkerhetsgjennomgangen.""" import io import pytest from PIL import Image from soapbox import db from soapbox.images import ImageError, process_image from tests.conftest import csrf, login def _owner_draft(client, cfg): login(client) r = client.post("/admin/new", data={"csrf": csrf(client)}) pid = r.headers["Location"].split("/admin/edit/")[1] client.post( f"/admin/edit/{pid}", data={"csrf": csrf(client), "title": "Hemmelig", "body": "TOPPHEMMELIG", "action": "save"}, ) (cfg.content_dir / "posts/hemmelig/priv.png").write_bytes(b"x") client.post("/admin/logout", data={"csrf": csrf(client)}) return "_owner/hemmelig" def test_guest_cannot_traverse_to_owner_post(client, cfg): _owner_draft(client, cfg) login(client, "gjest1") client.post("/admin/new", data={"csrf": csrf(client)}) # så guests/gjest1/ finnes token = csrf(client) for bad in ( "gjest1/../../posts/hemmelig", "gjest1/%2e%2e/%2e%2e/posts/hemmelig", "_owner/hemmelig", "../posts/hemmelig", ): assert client.get(f"/admin/edit/{bad}").status_code in (403, 404), bad assert client.get(f"/admin/history/{bad}").status_code in (403, 404), bad assert client.get(f"/admin/media/{bad}/priv.png").status_code in (403, 404), bad r = client.post( f"/admin/upload/{bad}", data={"file": (io.BytesIO(b"x"), "e.png")}, headers={"X-CSRF": token}, content_type="multipart/form-data", ) assert r.status_code in (403, 404), bad assert "TOPPHEMMELIG" not in client.get("/admin/edit/gjest1/../../posts/hemmelig").text assert not (cfg.content_dir / "posts/hemmelig/e.png").exists() def test_login_requires_csrf(client): r = client.post("/admin/login", data={"username": "eier", "password": "hemmelig123"}) assert r.status_code == 403 def test_login_backoff(client): client.get("/admin/login") token = csrf(client) for _ in range(5): client.post("/admin/login", data={"username": "eier", "password": "feil", "csrf": token}) r = client.post( "/admin/login", data={"username": "eier", "password": "hemmelig123", "csrf": token} ) assert r.status_code == 429 from soapbox import admin admin._login_failures.clear() def test_password_change_invalidates_other_sessions(app, client, cfg): login(client) other = app.test_client() login(other) assert other.get("/admin/").status_code == 200 conn = db.connect(cfg.db_path) db.set_password(conn, db.get_user_by_name(conn, "eier").id, "nyttpassord1") conn.close() assert other.get("/admin/").status_code == 302 # logget ut @pytest.mark.filterwarnings("ignore::PIL.Image.DecompressionBombWarning") def test_decompression_bomb_rejected(): # Liten PNG som deklarerer 8000x8000 = 64 MP > grensen buf = io.BytesIO() Image.new("1", (8000, 8000)).save(buf, "PNG") try: process_image(buf.getvalue(), 2000) except ImageError as e: assert "for stort" in str(e) else: raise AssertionError("skulle vært avvist") def test_security_headers(client): r = client.get("/") assert "frame-ancestors 'none'" in r.headers["Content-Security-Policy"] assert r.headers["X-Frame-Options"] == "DENY" login(client) assert "unsafe-inline" not in client.get("/admin/").headers["Content-Security-Policy"] def test_guest_username_cannot_shadow_owner_post(client, cfg): login(client) token = csrf(client) r = client.post("/admin/new", data={"csrf": token}) pid = r.headers["Location"].split("/admin/edit/")[1] client.post( f"/admin/edit/{pid}", data={"csrf": token, "title": "foo", "body": "x", "action": "publish"} ) client.post( "/admin/users", data={"csrf": token, "action": "create", "username": "foo", "password": "passord123"}, ) conn = db.connect(cfg.db_path) assert db.get_user_by_name(conn, "foo") is None conn.close() # og motsatt: eierens innlegg kan ikke få slug lik et gjestenavn 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": "gjest1", "body": "x", "action": "publish"}, ) assert r.headers["Location"].endswith("/admin/edit/_owner/gjest1-2")