From ba337855d594490fe75d79ac384e8e5303427515 Mon Sep 17 00:00:00 2001 From: phil Date: Tue, 6 Oct 2026 10:48:58 +0900 Subject: [PATCH] fix(security): Hide exception text and bound HTML strip regex CodeQL py/stack-trace-exposure: order sync, flow execution and directory batch registration returned str(exception) to API clients. Return fixed messages instead; details stay in server logs (logger.exception) and integration_logs. CodeQL py/polynomial-redos: _strip_html used <[^>]+>, which is O(n^2) on input like "<<<<...". Excluding '<' from the tag body makes it linear. Measured on 100k '<': 6.0s before, 0.001s after. Adds a regression test with that input. Co-Authored-By: Claude Fable 5.1 Signed-off-by: phil --- .../services/document_lifecycle_service.py | 3 ++- .../unit/test_document_lifecycle_service.py | 16 ++++++++++++++++ .../services/flow_executor_service.py | 5 ++++- .../services/directory_search_service.py | 7 +++++-- .../ecommerce/services/order_sync_service.py | 6 ++++-- 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/services/collab/documents/oneerp_documents_app/services/document_lifecycle_service.py b/services/collab/documents/oneerp_documents_app/services/document_lifecycle_service.py index 2ac9eea..f365442 100644 --- a/services/collab/documents/oneerp_documents_app/services/document_lifecycle_service.py +++ b/services/collab/documents/oneerp_documents_app/services/document_lifecycle_service.py @@ -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: diff --git a/services/collab/documents/tests/unit/test_document_lifecycle_service.py b/services/collab/documents/tests/unit/test_document_lifecycle_service.py index cb3a249..bb21b3e 100644 --- a/services/collab/documents/tests/unit/test_document_lifecycle_service.py +++ b/services/collab/documents/tests/unit/test_document_lifecycle_service.py @@ -6,6 +6,7 @@ from __future__ import annotations +import time from datetime import UTC, datetime from unittest.mock import MagicMock @@ -18,6 +19,10 @@ _strip_html, ) +# 이전 정규식은 이 길이에서 O(n²) 로 수 초 걸렸다. +_REDOS_INPUT_LEN = 100_000 +_REDOS_BUDGET_SEC = 1.0 + class TestDocumentLifecycleService: """DocumentLifecycleService 테스트.""" @@ -348,6 +353,17 @@ def test_HTML_태그_제거(self) -> None: assert _strip_html("굵은 기울임") == "굵은 기울임" 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"]) diff --git a/services/platform/integration-hub/oneerp_integration_hub_app/services/flow_executor_service.py b/services/platform/integration-hub/oneerp_integration_hub_app/services/flow_executor_service.py index 893b2fe..19d37dd 100644 --- a/services/platform/integration-hub/oneerp_integration_hub_app/services/flow_executor_service.py +++ b/services/platform/integration-hub/oneerp_integration_hub_app/services/flow_executor_service.py @@ -10,6 +10,9 @@ logger = logging.getLogger(__name__) +# 클라이언트용 고정 오류 문구 — 상세는 서버 로그와 integration_logs 에만 둔다. +_FLOW_FAILED = "플로우 실행 실패" + class FlowExecutorService: """통합 플로우 실행 비즈니스 로직. @@ -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, } diff --git a/services/portal/portal_comms/oneerp_portal_comms_app/directory/services/directory_search_service.py b/services/portal/portal_comms/oneerp_portal_comms_app/directory/services/directory_search_service.py index 6d9531c..cf29025 100644 --- a/services/portal/portal_comms/oneerp_portal_comms_app/directory/services/directory_search_service.py +++ b/services/portal/portal_comms/oneerp_portal_comms_app/directory/services/directory_search_service.py @@ -15,6 +15,9 @@ logger = logging.getLogger(__name__) +# 배치 응답용 고정 문구 — 예외 문자열을 클라이언트에 흘리지 않는다. +_ERR_PRIMARY_EXISTS = "ERR-DIR-009: 이미 주 소속이 존재합니다" + class DirectorySearchService: """인명부 검색 비즈니스 로직. @@ -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 diff --git a/services/sales/commerce/oneerp_commerce_app/ecommerce/services/order_sync_service.py b/services/sales/commerce/oneerp_commerce_app/ecommerce/services/order_sync_service.py index 8718757..4c38e59 100644 --- a/services/sales/commerce/oneerp_commerce_app/ecommerce/services/order_sync_service.py +++ b/services/sales/commerce/oneerp_commerce_app/ecommerce/services/order_sync_service.py @@ -13,6 +13,8 @@ logger = logging.getLogger(__name__) _MPO_PREFIX = "MPO" +# 클라이언트용 고정 오류 문구 — 예외 상세는 서버 로그에만 남긴다. +_SYNC_FAILED = "주문 저장 실패" class OrderSyncService: @@ -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(