Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@

def _strip_html(html: str) -> str:
"""HTML 태그를 제거하여 플레인텍스트로 변환한다."""
return re.sub(r"<[^>]+>", "", html).strip()
# 태그 본문에서 '<' 를 배제해 '<<<…' 입력의 O(n²) 역추적을 막는다.
return re.sub(r"<[^<>]+>", "", html).strip()


def _compute_document_hash(title: str, content: str, file_checksums: list[str]) -> str:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from __future__ import annotations

import time
from datetime import UTC, datetime
from unittest.mock import MagicMock

Expand All @@ -18,6 +19,10 @@
_strip_html,
)

# 이전 정규식은 이 길이에서 O(n²) 로 수 초 걸렸다.
_REDOS_INPUT_LEN = 100_000
_REDOS_BUDGET_SEC = 1.0


class TestDocumentLifecycleService:
"""DocumentLifecycleService 테스트."""
Expand Down Expand Up @@ -348,6 +353,17 @@ def test_HTML_태그_제거(self) -> None:
assert _strip_html("<b>굵은</b> <i>기울임</i>") == "굵은 기울임"
assert _strip_html("") == ""

def test_HTML_태그_제거_ReDoS_방지(self) -> None:
"""'<' 반복 입력도 선형 시간에 처리한다 (py/polynomial-redos)."""
adversarial = "<" * _REDOS_INPUT_LEN

started = time.perf_counter()
result = _strip_html(adversarial)
elapsed = time.perf_counter() - started

assert result == adversarial
assert elapsed < _REDOS_BUDGET_SEC

def test_문서_해시_계산(self) -> None:
"""동일 입력 시 동일한 SHA-256 해시를 반환한다."""
h1 = _compute_document_hash("제목", "내용", ["checksum1"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

logger = logging.getLogger(__name__)

# 클라이언트용 고정 오류 문구 — 상세는 서버 로그와 integration_logs 에만 둔다.
_FLOW_FAILED = "플로우 실행 실패"


class FlowExecutorService:
"""통합 플로우 실행 비즈니스 로직.
Expand Down Expand Up @@ -142,7 +145,7 @@ def execute_flow(self, flow_id: str) -> dict[str, Any]:
return {
"flow_id": flow_id,
"status": "failed",
"error": error_msg,
"error": _FLOW_FAILED,
"duration_ms": duration_ms,
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

logger = logging.getLogger(__name__)

# 배치 응답용 고정 문구 — 예외 문자열을 클라이언트에 흘리지 않는다.
_ERR_PRIMARY_EXISTS = "ERR-DIR-009: 이미 주 소속이 존재합니다"


class DirectorySearchService:
"""인명부 검색 비즈니스 로직.
Expand Down Expand Up @@ -202,8 +205,8 @@ def batch_register(
# DB 기존 주 소속 중복 검사
try:
self.validate_primary_assignment(employee_id)
except ValueError as e:
errors.append({"index": idx, "error": str(e)})
except ValueError:
errors.append({"index": idx, "error": _ERR_PRIMARY_EXISTS})
continue

entry["tenant_id"] = self._tenant_id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
logger = logging.getLogger(__name__)

_MPO_PREFIX = "MPO"
# 클라이언트용 고정 오류 문구 — 예외 상세는 서버 로그에만 남긴다.
_SYNC_FAILED = "주문 저장 실패"


class OrderSyncService:
Expand Down Expand Up @@ -84,8 +86,8 @@ def sync_orders(
}
self._order_repo.insert(doc)
created += 1
except Exception as e:
errors.append(f"{ext_id}: {e}")
except Exception:
errors.append(f"{ext_id}: {_SYNC_FAILED}")
logger.exception("주문 동기화 실패: %s", ext_id)

logger.info(
Expand Down
Loading