soapbox/tests/test_security.py

124 lines
4.4 KiB
Python
Raw Permalink Normal View History

"""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")