From a1b7b4873e219365fa180bc209fd0be18771acea Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 14 Jul 2026 02:02:52 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fix=20data=20leak=20in=20public=20export=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공개된 공유 링크를 통해 스키마 스냅샷을 내보낼 때, 민감한 메타데이터(코멘트, 예시 값 등)가 그대로 노출되는 취약점을 수정했습니다. _redact_sensitive_snapshot_fields 함수를 사용하여 변환(SQL 생성 또는 LLM 프롬프트) 전에 민감한 필드를 마스킹하도록 개선했습니다. --- .Jules/sentinel.md | 7 ++++++- .gitignore | 2 ++ backend/app/api/share.py | 10 +++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.Jules/sentinel.md b/.Jules/sentinel.md index 7d5df470..9ddeb53c 100644 --- a/.Jules/sentinel.md +++ b/.Jules/sentinel.md @@ -1,4 +1,9 @@ -## $(date +%Y-%m-%d) - Redact Sensitive Schema Comments in Public Shares +## 2026-07-14 - Redact Sensitive Schema Comments in Public Shares **Vulnerability:** Publicly shared schema snapshots (via `/api/share/...`) returned the entire JSON payload, which could expose sensitive internal schema comments (`comment`, `relation_comment`, `column_comment`) or sensitive data in `example_value` fields. **Learning:** When generating share links, only specific fields should be exposed, but we export the entire `snapshot_json` from the database. A recursive sanitizer function must be applied to scrub IDOR/data leakage vectors before returning the JSON payload. **Prevention:** Apply a recursive masking function (`_redact_sensitive_snapshot_fields`) on database JSON artifacts in read-only public endpoints. + +## 2026-07-14 - [CRITICAL] Fix Data Leak in Public Export Endpoints +**Vulnerability:** Unauthenticated share link endpoints for SQL export, DB reversing spec, and index design spec were directly passing un-redacted snapshot JSON data to the generators. This could expose sensitive metadata like `comment`, `relation_comment`, `column_comment`, and `example_value`. +**Learning:** Even when exposing data through format transformers (like SQL generators or LLM prompts), any sensitive metadata fields included in the base JSON payload can leak if not explicitly redacted before transformation. +**Prevention:** Always apply the `_redact_sensitive_snapshot_fields` utility (or equivalent redaction logic) to JSON payloads before passing them to any export or transformation function exposed via unauthenticated endpoints. diff --git a/.gitignore b/.gitignore index be445549..da1cb8f7 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ frontend/coverage/ registered_agents.json task_agent_mapping.json backend/venv/ +myenv/ +test_venv/ diff --git a/backend/app/api/share.py b/backend/app/api/share.py index 2a9a4c6a..073da0a8 100644 --- a/backend/app/api/share.py +++ b/backend/app/api/share.py @@ -172,7 +172,7 @@ async def export_shared_snapshot_sql( data = await session.get(SchemaSnapshotData, schema_snapshot_uuid) if data is None: return "-- snapshot data not found\n" - return snapshot_json_to_sql(data.snapshot_json, target_dialect=dialect) + return snapshot_json_to_sql(_redact_sensitive_snapshot_fields(data.snapshot_json), target_dialect=dialect) @router.get( @@ -203,7 +203,7 @@ async def export_shared_snapshot_reversing_spec( return "# DB Reversing Specification\n\nSnapshot data not found.\n" if mode == "llm-draft": try: - return await generate_reversing_llm_draft(data.snapshot_json) + return await generate_reversing_llm_draft(_redact_sensitive_snapshot_fields(data.snapshot_json)) except LlmConfigurationError as exc: raise HTTPException( status_code=503, detail="LLM configuration error" @@ -212,7 +212,7 @@ async def export_shared_snapshot_reversing_spec( raise HTTPException( status_code=502, detail="LLM provider request failed" ) from exc - return generate_reversing_spec(data.snapshot_json, mode=mode) + return generate_reversing_spec(_redact_sensitive_snapshot_fields(data.snapshot_json), mode=mode) @router.get( @@ -243,7 +243,7 @@ async def export_shared_snapshot_index_design( return "# ERD Index Design\n\nSnapshot data not found.\n" if mode == "llm-draft": try: - return await generate_index_design_llm_draft(data.snapshot_json) + return await generate_index_design_llm_draft(_redact_sensitive_snapshot_fields(data.snapshot_json)) except LlmConfigurationError as exc: raise HTTPException( status_code=503, detail="LLM configuration error" @@ -252,4 +252,4 @@ async def export_shared_snapshot_index_design( raise HTTPException( status_code=502, detail="LLM provider request failed" ) from exc - return generate_index_design_spec(data.snapshot_json, mode=mode) + return generate_index_design_spec(_redact_sensitive_snapshot_fields(data.snapshot_json), mode=mode) From 8f2af1fe9b545b97b34dce64d08a94c2985e6000 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 14 Jul 2026 02:05:58 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fix=20data=20leak=20in=20public=20export=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공개된 공유 링크를 통해 스키마 스냅샷을 내보낼 때, 민감한 메타데이터(코멘트, 예시 값 등)가 그대로 노출되는 취약점을 수정했습니다. _redact_sensitive_snapshot_fields 함수를 사용하여 변환(SQL 생성 또는 LLM 프롬프트) 전에 민감한 필드를 마스킹하도록 개선했습니다. 또한 CI 환경의 타입 검증(mypy)을 통과하기 위해 타입 캐스팅(cast)을 추가했습니다. --- backend/app/api/share.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/app/api/share.py b/backend/app/api/share.py index 073da0a8..15b41e7c 100644 --- a/backend/app/api/share.py +++ b/backend/app/api/share.py @@ -2,6 +2,7 @@ import datetime as dt import uuid +from typing import cast from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import PlainTextResponse @@ -140,7 +141,7 @@ async def get_shared_snapshot( "status": snap.status, "schema_filter": snap.schema_filter, "error_message": snap.error_message, - "snapshot_json": _redact_sensitive_snapshot_fields(data.snapshot_json) + "snapshot_json": cast(dict, _redact_sensitive_snapshot_fields(data.snapshot_json)) if data else None, } @@ -172,7 +173,7 @@ async def export_shared_snapshot_sql( data = await session.get(SchemaSnapshotData, schema_snapshot_uuid) if data is None: return "-- snapshot data not found\n" - return snapshot_json_to_sql(_redact_sensitive_snapshot_fields(data.snapshot_json), target_dialect=dialect) + return snapshot_json_to_sql(cast(dict, _redact_sensitive_snapshot_fields(data.snapshot_json)), target_dialect=dialect) @router.get( @@ -203,7 +204,7 @@ async def export_shared_snapshot_reversing_spec( return "# DB Reversing Specification\n\nSnapshot data not found.\n" if mode == "llm-draft": try: - return await generate_reversing_llm_draft(_redact_sensitive_snapshot_fields(data.snapshot_json)) + return await generate_reversing_llm_draft(cast(dict, _redact_sensitive_snapshot_fields(data.snapshot_json))) except LlmConfigurationError as exc: raise HTTPException( status_code=503, detail="LLM configuration error" @@ -212,7 +213,7 @@ async def export_shared_snapshot_reversing_spec( raise HTTPException( status_code=502, detail="LLM provider request failed" ) from exc - return generate_reversing_spec(_redact_sensitive_snapshot_fields(data.snapshot_json), mode=mode) + return generate_reversing_spec(cast(dict, _redact_sensitive_snapshot_fields(data.snapshot_json)), mode=mode) @router.get( @@ -243,7 +244,7 @@ async def export_shared_snapshot_index_design( return "# ERD Index Design\n\nSnapshot data not found.\n" if mode == "llm-draft": try: - return await generate_index_design_llm_draft(_redact_sensitive_snapshot_fields(data.snapshot_json)) + return await generate_index_design_llm_draft(cast(dict, _redact_sensitive_snapshot_fields(data.snapshot_json))) except LlmConfigurationError as exc: raise HTTPException( status_code=503, detail="LLM configuration error" @@ -252,4 +253,4 @@ async def export_shared_snapshot_index_design( raise HTTPException( status_code=502, detail="LLM provider request failed" ) from exc - return generate_index_design_spec(_redact_sensitive_snapshot_fields(data.snapshot_json), mode=mode) + return generate_index_design_spec(cast(dict, _redact_sensitive_snapshot_fields(data.snapshot_json)), mode=mode) From 50019bc2ce65c42a0d44320e2067b31b463e5156 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 14 Jul 2026 02:20:40 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITIC?= =?UTF-8?q?AL]=20Fix=20data=20leak=20in=20public=20export=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공개된 공유 링크를 통해 스키마 스냅샷을 내보낼 때, 민감한 메타데이터(코멘트, 예시 값 등)가 그대로 노출되는 취약점을 수정했습니다. _redact_sensitive_snapshot_fields 함수를 사용하여 변환(SQL 생성 또는 LLM 프롬프트) 전에 민감한 필드를 마스킹하도록 개선했습니다. 또한 CI 환경의 타입 검증(mypy)을 통과하기 위해 타입 캐스팅(cast)을 추가했고, Strix 보안 점검을 통과하도록 문자열 내의 DSN 정보를 안전하게 마스킹하는 처리를 추가했습니다. --- backend/app/api/share.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/backend/app/api/share.py b/backend/app/api/share.py index 15b41e7c..965b8f1b 100644 --- a/backend/app/api/share.py +++ b/backend/app/api/share.py @@ -30,6 +30,12 @@ router = APIRouter(prefix="/api", tags=["share"]) +import re +from app.dsn_redaction import redact_dsn_error_message + +import re +from app.dsn_redaction import redact_dsn_error_message + def _redact_sensitive_snapshot_fields( data: dict | list | str | int | float | bool | None, ) -> dict | list | str | int | float | bool | None: @@ -37,12 +43,26 @@ def _redact_sensitive_snapshot_fields( if isinstance(data, dict): return { k: "***" - if k in {"comment", "relation_comment", "column_comment", "example_value"} + if isinstance(k, str) and ( + k.lower() in {"comment", "relation_comment", "column_comment", "example_value", "default_value"} or + "password" in k.lower() or + "secret" in k.lower() or + "credential" in k.lower() or + "token" in k.lower() or + "key" in k.lower() + ) else _redact_sensitive_snapshot_fields(v) for k, v in data.items() } elif isinstance(data, list): return [_redact_sensitive_snapshot_fields(v) for v in data] + elif isinstance(data, str): + if "://" in data or "@" in data: + try: + from app.dsn_redaction import redact_dsn_error_message + return redact_dsn_error_message(data, "") + except Exception: + pass return data