- H1: 구조물 저장 structure_id 를 안전 이름 규칙(FOLDER_PATTERN 과 한 벌)으로 검사 · 어기면 422 - H1: 복사본 맞춤에서 한 번 더 — 규칙 밖 · base 밖 폴더는 지우기 · 쓰기 건너뜀(못만듦) - H2: filter_key · method 를 허용 목록(Literal)으로 · 파일 이름 만드는 곳(표고 sampler · 비용면 · 스켈레톤)에서 한 번 더 - 시험: test_54_22_security 26 통과 · 관 간격 시험 가짜 필터 이름을 허용 값으로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
110 lines
4.2 KiB
Python
110 lines
4.2 KiB
Python
"""54-22 보안 — H1 구조물 id 경로 넘기 · H2 지면 필터 · 표현 이름 경로 넘기.
|
|
|
|
못박는 것
|
|
- H1: 구조물 저장 `structure_id` 는 안전 이름만(어기면 검사 실패 = 422) · 복사본 맞춤은
|
|
규칙 밖 · `base` 밖 폴더를 지우지도 쓰지도 않음.
|
|
- H2: `filter_key` · `method` 는 허용 목록만(스키마) · 파일 이름 만드는 곳에서도 한 번 더.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from B05_Profile.B05_Profile_Engine_Skeleton import load_or_build_skeleton
|
|
from B05_Profile.B05_Profile_Engine_Solver import _load_dtm_grid, _source_npz_paths
|
|
from B05_Profile.B05_Profile_Schema import RouteConfirmRequest, RouteSolveRequest
|
|
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance
|
|
from common_util.common_util_surface_sampler import build_surface_sampler, check_surface_keys
|
|
from M02_MasterTemplete import M02_Item_Snapshot as snap
|
|
|
|
BAD_IDS = ["../../..", "..", "a/b", "a\\b", "C:/Windows", "/etc", ".hidden", "", "x" * 65]
|
|
BAD_KEYS = ["../other/dtm_csf", "csf/../../x", "C:/x", "", "CSF"]
|
|
|
|
|
|
def _structure(structure_id):
|
|
return StructureInstance(
|
|
structure_id=structure_id, type_id="x", placement="point", chainage_m=1
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("bad", BAD_IDS)
|
|
def test_H1_악성_id_거절(bad):
|
|
with pytest.raises(ValidationError):
|
|
_structure(bad)
|
|
|
|
|
|
@pytest.mark.parametrize("good", [None, "0c3d99cc5d8f49139c871d662537da42", "abc_1-2", "s1__g1"])
|
|
def test_H1_정상_id_통과(good):
|
|
assert _structure(good).structure_id == good
|
|
|
|
|
|
def test_H1_safe_folder(tmp_path):
|
|
base = tmp_path / "templates" / "structures"
|
|
base.mkdir(parents=True)
|
|
assert snap.safe_folder(base, "abc123") == base / "abc123"
|
|
assert snap.safe_folder(base, "pipe@40.0__gin") == base / "pipe@40.0__gin"
|
|
for bad in BAD_IDS[:-1] + [str(tmp_path)]: # 길이는 스키마 몫
|
|
assert snap.safe_folder(base, bad) is None
|
|
|
|
|
|
def test_H1_맞춤이_base_밖을_안_건드림(tmp_path, monkeypatch):
|
|
root = tmp_path / "proj"
|
|
base = snap.structures_dir(root)
|
|
base.mkdir(parents=True)
|
|
victim = root / "victim" # base / "../../victim"
|
|
victim.mkdir()
|
|
(victim / "keep.txt").write_text("살아야 함", encoding="utf-8")
|
|
item = {"layer": "master", "owner": "", "key": "k"}
|
|
monkeypatch.setattr(
|
|
snap, "assignments", lambda _root: {"../../victim": item, str(victim): item, "ok1": item}
|
|
)
|
|
|
|
def fake_write(target: Path, *_args):
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
return {}
|
|
|
|
monkeypatch.setattr(snap, "write_snapshot", fake_write)
|
|
result = snap.sync_snapshots(root)
|
|
assert (victim / "keep.txt").is_file()
|
|
assert set(result["못만듦"]) == {"../../victim", str(victim)}
|
|
assert result["만듦"] == ["ok1"]
|
|
assert result["지움"] == []
|
|
|
|
|
|
@pytest.mark.parametrize("bad", BAD_KEYS)
|
|
def test_H2_스키마_허용_목록(bad):
|
|
body = {"bp": {"x": 0, "y": 0}, "ep": {"x": 1, "y": 1}}
|
|
with pytest.raises(ValidationError):
|
|
RouteSolveRequest(filter_key=bad, **body)
|
|
with pytest.raises(ValidationError):
|
|
RouteSolveRequest(filter_key="csf", method=bad, **body)
|
|
with pytest.raises(ValidationError):
|
|
RouteConfirmRequest(filter_key=bad)
|
|
with pytest.raises(ValidationError):
|
|
RouteConfirmRequest(method=bad)
|
|
|
|
|
|
def test_H2_스키마_정상값_통과():
|
|
assert RouteConfirmRequest(filter_key="grid_min_z", method="tin").method == "tin"
|
|
assert RouteConfirmRequest().filter_key is None
|
|
|
|
|
|
@pytest.mark.parametrize("bad", BAD_KEYS)
|
|
def test_H2_파일_이름_만드는_곳(bad, tmp_path):
|
|
with pytest.raises(ValueError):
|
|
check_surface_keys(bad)
|
|
with pytest.raises(ValueError):
|
|
check_surface_keys("csf", bad)
|
|
with pytest.raises(ValueError):
|
|
build_surface_sampler(tmp_path, bad, "dtm", False)
|
|
with pytest.raises(ValueError):
|
|
_load_dtm_grid(tmp_path, bad, False)
|
|
with pytest.raises(ValueError):
|
|
_source_npz_paths(tmp_path, "csf", bad, False)
|
|
with pytest.raises(ValueError):
|
|
load_or_build_skeleton(tmp_path, bad, "dtm", False)
|
|
check_surface_keys("classification", "meshfree")
|