fix(B05): 54-22 H1 · H2 경로 넘기 막음 — 구조물 id 안전 이름 · 지면 필터 · 표현 허용 목록
- 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
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"""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")
|
||||
@@ -71,7 +71,7 @@ def test_request_wins_over_stored():
|
||||
def test_schema_defaults_to_none_and_ships_the_flag():
|
||||
"""스키마 기본은 None(미지정) — 저장값을 덮어쓰지 않는다."""
|
||||
request = RouteSolveRequest(
|
||||
filter_key="f",
|
||||
filter_key="csf",
|
||||
bp={"x": 0.0, "y": 0.0},
|
||||
ep={"x": 100.0, "y": 0.0},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user