summaryrefslogtreecommitdiff
path: root/tests/test_security.py
blob: f929bb8d6161c5f1befa909846839fb44bddcb51 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"""Security regression tests."""

from __future__ import annotations

import re
import shutil
import uuid
from pathlib import Path

import pytest
from starlette.testclient import TestClient

from ollama_web.app import create_app
from ollama_web.config import settings


@pytest.fixture
def workspace_tmp() -> Path:
    path = Path("data/.pytest-security") / uuid.uuid4().hex
    path.mkdir(parents=True, exist_ok=True)
    try:
        yield path
    finally:
        shutil.rmtree(path, ignore_errors=True)


def _client(workspace_tmp: Path, monkeypatch) -> TestClient:
    monkeypatch.setattr(settings, "data_dir", str(workspace_tmp))
    monkeypatch.setattr(settings, "pin", "123456")
    monkeypatch.setattr(settings, "secret_key", "test-secret")
    monkeypatch.setattr(settings, "allowed_origins", [])
    return TestClient(create_app())


def test_api_requires_login(workspace_tmp: Path, monkeypatch) -> None:
    client = _client(workspace_tmp, monkeypatch)
    res = client.get("/api/sessions")
    assert res.status_code == 401


def test_login_and_csrf_protect_state_changes(workspace_tmp: Path, monkeypatch) -> None:
    client = _client(workspace_tmp, monkeypatch)

    assert client.post("/api/auth/login", json={"pin": "bad"}).status_code == 401

    login = client.post("/api/auth/login", json={"pin": "123456"})
    assert login.status_code == 200
    csrf = login.headers["x-csrf-token"]

    assert client.post("/api/sessions").status_code == 403

    created = client.post("/api/sessions", headers={"X-CSRF-Token": csrf})
    assert created.status_code == 200
    assert created.json()["id"]


def test_cors_wildcard_is_not_enabled_by_default(workspace_tmp: Path, monkeypatch) -> None:
    client = _client(workspace_tmp, monkeypatch)
    res = client.get("/api/auth/status", headers={"Origin": "https://evil.example"})
    assert res.headers.get("access-control-allow-origin") != "*"


def test_invalid_session_id_is_rejected_after_login(workspace_tmp: Path, monkeypatch) -> None:
    client = _client(workspace_tmp, monkeypatch)
    login = client.post("/api/auth/login", json={"pin": "123456"})
    csrf = login.headers["x-csrf-token"]

    res = client.delete("/api/sessions/../x", headers={"X-CSRF-Token": csrf})
    assert res.status_code in {400, 404}


def test_private_urls_are_blocked() -> None:
    from ollama_web.tools.helper.safe_http import UnsafeURL, validate_public_http_url

    for url in (
        "http://127.0.0.1:11434",
        "http://localhost:8000",
        "http://192.168.1.1",
        "http://10.0.0.1",
        "http://169.254.169.254/latest/meta-data",
    ):
        try:
            validate_public_http_url(url)
        except UnsafeURL:
            continue
        raise AssertionError(f"URL should have been blocked: {url}")


def test_pdf_to_text_does_not_read_local_paths(workspace_tmp: Path) -> None:
    from ollama_web.tools.pdf import pdf_to_text

    secret = workspace_tmp / "secret.pdf"
    secret.write_bytes(b"%PDF-1.4\n")
    assert pdf_to_text(str(secret)) == "Only http(s) PDF URLs are supported."


def test_frontend_uses_local_sanitizer_and_no_cdn() -> None:
    html = Path("src/ollama_web/templates/index.html").read_text(encoding="utf-8")
    app_js = Path("src/ollama_web/static/app.js").read_text(encoding="utf-8")

    assert "cdnjs.cloudflare.com" not in html
    assert "vendor/purify.min.js" in html
    assert "DOMPurify.sanitize" in app_js


def test_katex_local_font_assets_are_present() -> None:
    css_path = Path("src/ollama_web/static/vendor/katex.min.css")
    vendor_dir = css_path.parent
    css = css_path.read_text(encoding="utf-8")
    font_paths = sorted(set(re.findall(r"fonts/KaTeX_[^)'\"\s]+", css)))

    assert font_paths
    assert not [path for path in font_paths if not (vendor_dir / path).exists()]