Skip to content
Open
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
7 changes: 6 additions & 1 deletion .Jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ frontend/coverage/
registered_agents.json
task_agent_mapping.json
backend/venv/
myenv/
test_venv/
35 changes: 28 additions & 7 deletions backend/app/api/share.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -29,19 +30,39 @@
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:
"""Redact sensitive fields from snapshot JSON payload when shared publicly."""
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


Expand Down Expand Up @@ -140,7 +161,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,
}
Expand Down Expand Up @@ -172,7 +193,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(cast(dict, _redact_sensitive_snapshot_fields(data.snapshot_json)), target_dialect=dialect)


@router.get(
Expand Down Expand Up @@ -203,7 +224,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(cast(dict, _redact_sensitive_snapshot_fields(data.snapshot_json)))
except LlmConfigurationError as exc:
raise HTTPException(
status_code=503, detail="LLM configuration error"
Expand All @@ -212,7 +233,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(cast(dict, _redact_sensitive_snapshot_fields(data.snapshot_json)), mode=mode)


@router.get(
Expand Down Expand Up @@ -243,7 +264,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(cast(dict, _redact_sensitive_snapshot_fields(data.snapshot_json)))
except LlmConfigurationError as exc:
raise HTTPException(
status_code=503, detail="LLM configuration error"
Expand All @@ -252,4 +273,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(cast(dict, _redact_sensitive_snapshot_fields(data.snapshot_json)), mode=mode)
Loading