"""M11 §19-§20: the H-series as an integrated release run. The H tests have had coverage since M2, and it is good: `test_egress.py` fails if a bulk load names a heavy column, `test_endpoint_policy.py` walks the address rules, `test_tls_trust.py` fails if verification is weakened. What M11 adds is the part those files were never asked for: * the checks that only make sense **against the assembled product** — a tampered database refused at request time, a wildcard CORS origin refused at startup, an unknown API path that is a 404 rather than the SPA; * the ones whose answer is **"not applicable, and here is the proof"** — H09, which the acceptance text itself makes conditional on archive extraction existing; * the ones where an M11 change could have opened something — the context-window probe is a new outbound request, and it must obey the same policy as inference. Browser-side security (stored XSS, `javascript:` URLs, hostile Markdown, the CSP, hidden knowledge in the DOM) is in `tools/m11_browser.py`, because those are claims about a rendered page and a unit test asserting them would be asserting about a string. python -m pytest tests/test_m11_security.py -v """ import asyncio import importlib import json import os import pathlib import subprocess import sys import pytest from fastapi import Depends from fastapi.testclient import TestClient from app import auth, contextwindow, endpoints, limits, models from app.database import Base, SessionLocal, engine, get_db from app.main import app from app.routers import adventures from fakes import ScriptedProvider BACKEND = pathlib.Path(__file__).resolve().parent.parent @pytest.fixture() def client(monkeypatch): Base.metadata.create_all(bind=engine) setup = SessionLocal() user = models.User(is_guest=False, email="m11sec@example.com") setup.add(user) setup.flush() setup.add(models.Settings(user_id=user.id, model="test-model", embedding_model="", max_output_tokens=400)) adventure = models.Adventure(user_id=user.id, title="Security") setup.add(adventure) setup.flush() setup.add(models.Action(adventure_id=adventure.id, type="start", text="Rain.")) setup.commit() adv_id, user_id = adventure.id, user.id setup.close() monkeypatch.setattr(limits, "check_row_cap", lambda *a, **k: None) monkeypatch.setattr(adventures.turns, "OpenAICompatibleProvider", ScriptedProvider) app.dependency_overrides[auth.get_current_user] = ( lambda db=Depends(get_db): db.get(models.User, user_id) ) test_client = TestClient(app) test_client.adv_id = adv_id test_client.user_id = user_id try: yield test_client finally: app.dependency_overrides.clear() adventures.turns._active_turns.clear() Base.metadata.drop_all(bind=engine) # ------------------------------------------------------------------- H09 def test_h09_the_product_extracts_no_archives(): """H09 is conditional, and this is the condition, checked rather than assumed. "REQUIRED FOR V1 **if ZIP import/export is implemented**". Nothing in the application opens an archive: the bundle is JSON and imported sources are single files. So H09 is NOT APPLICABLE — and this test is what keeps that true, because the day somebody adds an unzip, it fails and H09 becomes required again. """ offenders = [] for path in (BACKEND / "app").rglob("*.py"): body = path.read_text() for name in ("zipfile", "tarfile", "shutil.unpack_archive", "gzip.open", "py7zr", "rarfile"): if name in body: offenders.append(f"{path.name}: {name}") assert offenders == [], offenders def test_h09_an_upload_named_like_a_traversal_cannot_escape(client): """H08's sibling: the filename is metadata and never a path. Even with no archive extraction, an import takes a filename from the caller. It is stored, shown and exported — never joined to a directory. """ hostile = "../../../../etc/cron.d/pwned.md" response = client.post( f"/api/adventures/{client.adv_id}/knowledge", files={"file": (hostile, b"# nothing\n\ntext\n", "text/markdown")}, data={"classification": "reference"}, ) assert response.status_code == 201, response.text[:300] stored = response.json()["original_filename"] assert "/" not in stored and ".." not in stored, stored assert not pathlib.Path("/etc/cron.d/pwned.md").exists() # ------------------------------------------------------------------- H10 def test_h10_a_wildcard_cors_origin_refuses_to_start(tmp_path): """Startup refusal, proved by actually starting a process with it set. Importing the module in-process would not do: the check runs at import time, and a test that reached it through `importlib` would still be this process, with this process's environment. A real interpreter is the only honest way to ask "does the application refuse to come up". """ result = subprocess.run( [sys.executable, "-c", "import app.main"], cwd=str(BACKEND), capture_output=True, text=True, env={**os.environ, "AIDND_CORS_ORIGINS": "*", "AIDND_DB_PATH": str(tmp_path / "x.db"), "AIDND_DATABASE_URL": "", "DATABASE_URL": ""}, ) assert result.returncode != 0, "the application started with a wildcard origin" assert "must not contain" in (result.stderr + result.stdout) def test_h10_a_named_origin_is_accepted(tmp_path): """The control: the refusal above is about the wildcard, not about the var.""" result = subprocess.run( [sys.executable, "-c", "import app.main"], cwd=str(BACKEND), capture_output=True, text=True, env={**os.environ, "AIDND_CORS_ORIGINS": "http://127.0.0.1:5173", "AIDND_DB_PATH": str(tmp_path / "y.db"), "AIDND_DATABASE_URL": "", "DATABASE_URL": ""}, ) assert result.returncode == 0, result.stderr[-400:] def test_h10_an_unknown_api_path_is_a_404_not_the_spa(client): """A JSON API that answers HTML is one a client cannot tell has failed.""" response = client.get("/api/nothing-here") assert response.status_code == 404 assert "" in response.text or "