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
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
**Vulnerability:** User-provided string fields (like project and connection names) lacked strict validation against control characters, only relying on length constraints.
**Learning:** This could potentially lead to Log Injection (CRLF injection), Null Byte Injection, or terminal escape injection if these strings are subsequently logged or rendered directly.
**Prevention:** Use explicit regex validation `pattern=r'^[^\x00-\x1F\x7F]+$'` on Pydantic string fields to strictly reject control characters.

## 2025-02-18 - Credential Leak via Malformed DSN (urlsplit behavior)
**Vulnerability:** Python's `urllib.parse.urlsplit` fails to correctly populate the `netloc` component (and thus the `password` property) for DSN strings that do not contain `://` (e.g., `postgres:user:password@host/db`) or use non-standard schemes with underscores (e.g., `snowflake_invalid://`). As a result, credentials embedded in these types of DSNs bypassed the backend redaction logic (`dsn_redaction.py`) and could be leaked into error messages.
**Learning:** `urlsplit` places credentials in the `path` component instead of `netloc` for non-standard DSNs lacking `://`, rendering standard property access (`parsed.password`) useless for extraction.
**Prevention:** In DSN redaction or validation utilities, always include a fallback mechanism to detect and modify the scheme (e.g., substituting with a valid one like `http://` before parsing) to ensure `urlsplit` properly extracts secrets from these edge-case connection strings.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
- [FE] `autoInfer.ts`에 λŒ€ν•œ λ‹¨μœ„ ν…ŒμŠ€νŠΈ 및 UI μ»΄ν¬λ„ŒνŠΈ λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό μΆ”κ°€ν•˜μ—¬ 100% ν…ŒμŠ€νŠΈ 컀버리지λ₯Ό μœ μ§€ν•©λ‹ˆλ‹€.
- [FE] ⬇️ **DBML Export**: ERD λ‹€μ΄μ–΄κ·Έλž¨μ„ DBML (Database Markup Language) ν˜•μ‹μœΌλ‘œ 내보낼 수 μžˆλŠ” κΈ°λŠ₯을 μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€. μƒλ‹¨μ˜ DBML λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ λ‹€μš΄λ‘œλ“œν•  수 μžˆμŠ΅λ‹ˆλ‹€.
- [FE] πŸ“š **Data Dictionary Export**: ERD ν…Œμ΄λΈ”/컬럼 메타데이터λ₯Ό CSV 및 Markdown으둜 내보내며, CSV formula injectionκ³Ό Markdown λ Œλ”λ§ escapeλ₯Ό μ μš©ν–ˆμŠ΅λ‹ˆλ‹€.
- [BE] πŸ›‘οΈ **λ³΄μ•ˆ**: `urllib.parse.urlsplit`κ°€ `://`κ°€ μ—†κ±°λ‚˜ μ–Έλ”μŠ€μ½”μ–΄κ°€ ν¬ν•¨λœ λΉ„ν‘œμ€€ DSN λ¬Έμžμ—΄μ„ νŒŒμ‹±ν•  λ•Œ 인증 정보가 `netloc` λŒ€μ‹  `path`둜 μœ μΆœλ˜μ–΄ λ‘œκ·Έλ‚˜ μ—λŸ¬ λ©”μ‹œμ§€μ— λ…ΈμΆœλ˜λŠ” λ³΄μ•ˆ 취약점을 μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€.
10 changes: 7 additions & 3 deletions backend/app/dsn_redaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,13 @@ def _password_candidates_from_dsn(dsn: str) -> set[str]:
password: str | None = None
try:
parsed = urlsplit(dsn)
if "://" in dsn and not parsed.netloc:
# ponytail: keep urlsplit; only swap the non-RFC scheme so userinfo parses.
parsed = urlsplit("http://" + dsn.split("://", 1)[1])
if not parsed.netloc:
if "://" in dsn:
# ponytail: keep urlsplit; only swap the non-RFC scheme so userinfo parses.
parsed = urlsplit("http://" + dsn.split("://", 1)[1])
elif ":" in dsn:
# urlsplit fails to extract netloc if scheme lacks :// (e.g. postgres:user:pass@host)
parsed = urlsplit("http://" + dsn.split(":", 1)[1])
netloc = parsed.netloc
password = parsed.password
query = parsed.query
Expand Down
10 changes: 10 additions & 0 deletions backend/tests/test_dsn_redaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,13 @@ def test_malformed_dsn_still_redacts_embedded_secrets() -> None:
assert "s3cr3t" not in redacted
assert "q/secret" not in redacted
assert "password=***" in redacted

def test_redacts_dsn_without_slashes() -> None:
dsn = "postgres:user:s3cr3t@db.example.com/app?password=q%2Fsecret"
error = f"driver failed for s3cr3t with password=q/secret while using {dsn}"

redacted = redact_dsn_error_message(error, dsn)

assert "s3cr3t" not in redacted
assert "q/secret" not in redacted
assert "password=***" in redacted
Loading
Loading