Files
geointel/backend/tests/test_storage_containment.py
T
JensandClaude Opus 5 e2f586c029 consume only artifacts the runtime produced
tile_manifest_path arrives in the detection and segmentation request and was
read straight off disk, and a manifest entry may name an absolute tile path.
That makes an API field an unbounded reference to the host filesystem, and it
contradicts the rule the persistence model rests on: only a governed,
runtime-produced artifact may be consumed, and a file outside the storage root
is not one.

Both the manifest and every tile it names now resolve under STORAGE_ROOT.
Resolution happens before the comparison, so ".." cannot climb out and a
sibling that merely shares a name prefix does not pass.
GEOINTEL_ALLOW_EXTERNAL_ARTIFACT_PATHS opts out for provisioning workflows that
stage tiles before ingest.

The check honours the Settings the caller is operating under rather than the
process-wide ones, because every analysis path already threads its own.

The affected tests write manifests into tmp_path, so they now declare tmp_path
as the storage root — which is what a deployment does, and makes the fixtures
more honest than they were.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 16:16:25 +02:00

91 lines
3.3 KiB
Python

"""Analysis may only read artifacts the runtime itself produced.
``tile_manifest_path`` arrives in the detection and segmentation request and was
read straight off disk, and a manifest entry may name an absolute tile path. A
manifest outside the storage root is by definition not a governed artifact, so
consuming one contradicts the rule the whole persistence model rests on — and
it turns an API field into an unbounded reference to the host filesystem.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from app.core.errors import AppError
from app.services.storage_service import StorageService
def test_a_path_inside_the_root_is_returned_resolved(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setenv("STORAGE_ROOT", str(tmp_path))
target = tmp_path / "tiles" / "manifest.json"
target.parent.mkdir(parents=True)
target.write_text("{}", encoding="utf-8")
resolved = StorageService.assert_within_storage_root(str(target), label="tile manifest")
assert resolved == target.resolve()
def test_a_path_outside_the_root_is_refused(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setenv("STORAGE_ROOT", str(tmp_path / "storage"))
(tmp_path / "storage").mkdir()
outside = tmp_path / "elsewhere.json"
outside.write_text("{}", encoding="utf-8")
with pytest.raises(AppError) as exc_info:
StorageService.assert_within_storage_root(str(outside), label="tile manifest")
assert exc_info.value.code == "STORAGE_PATH_OUTSIDE_ROOT"
assert "tile manifest" in exc_info.value.message
def test_a_traversal_sequence_cannot_climb_out(tmp_path: Path, monkeypatch) -> None:
root = tmp_path / "storage"
root.mkdir()
monkeypatch.setenv("STORAGE_ROOT", str(root))
secret = tmp_path / "secret.json"
secret.write_text("{}", encoding="utf-8")
with pytest.raises(AppError) as exc_info:
StorageService.assert_within_storage_root(str(root / ".." / "secret.json"), label="tile")
assert exc_info.value.code == "STORAGE_PATH_OUTSIDE_ROOT"
def test_a_sibling_directory_sharing_a_name_prefix_is_refused(tmp_path: Path, monkeypatch) -> None:
"""``/data/storage-old`` is not inside ``/data/storage``."""
root = tmp_path / "storage"
root.mkdir()
sibling = tmp_path / "storage-old"
sibling.mkdir()
monkeypatch.setenv("STORAGE_ROOT", str(root))
target = sibling / "manifest.json"
target.write_text("{}", encoding="utf-8")
with pytest.raises(AppError):
StorageService.assert_within_storage_root(str(target), label="tile manifest")
def test_an_empty_path_is_refused(tmp_path: Path, monkeypatch) -> None:
monkeypatch.setenv("STORAGE_ROOT", str(tmp_path))
with pytest.raises(AppError):
StorageService.assert_within_storage_root("", label="tile manifest")
def test_the_check_can_be_disabled_for_an_operator_provisioning_workflow(
tmp_path: Path, monkeypatch
) -> None:
"""Provisioning scripts stage tiles outside the root before ingest."""
monkeypatch.setenv("STORAGE_ROOT", str(tmp_path / "storage"))
monkeypatch.setenv("GEOINTEL_ALLOW_EXTERNAL_ARTIFACT_PATHS", "true")
(tmp_path / "storage").mkdir()
outside = tmp_path / "elsewhere.json"
outside.write_text("{}", encoding="utf-8")
assert StorageService.assert_within_storage_root(str(outside), label="tile") == outside.resolve()