Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@
**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 - Hardening Pydantic String Fields Against Control Characters (Extended)
**Vulnerability:** User-provided string fields in several backend schemas (like diagram names and API key names) lacked strict validation against control characters.
**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. Blanket control filtering should NOT be applied to database identifiers (like schema or relation names) because PostgreSQL allows them (except NUL); filtering them breaks identifier integrity.
**Prevention:** Use explicit regex validation `pattern=r"^[^\x00-\x1F\x7F]+$"` on product label fields (e.g. diagram or API key names) to strictly reject control characters. Ensure this restriction is omitted from database identifiers and fields that legitimately require multiline inputs or complex formatting (like markdown bodies or SQL queries). If log injection is a concern for database identifiers, it must be fixed at the logging sink (via encoding/structured logging) rather than at the input schema level.
24 changes: 20 additions & 4 deletions backend/app/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,11 @@ class IndexRedundancyOut(BaseModel):
class DiagramViewCreateIn(BaseModel):
"""Request body for saving an ERD canvas view."""

name: str = Field(min_length=1, max_length=200)
name: str = Field(
min_length=1,
max_length=200,
pattern=r"^[^\x00-\x1F\x7F]+$",
)
# Opaque client layout (node positions, hidden tables, viewport). The API
# bounds the serialized size in the endpoint to prevent abuse.
layout_json: dict
Expand All @@ -214,8 +218,16 @@ class DiagramViewDetailOut(DiagramViewOut):
class TableAnnotationUpsertIn(BaseModel):
"""Request body for creating/updating a table annotation."""

schema_name: str = Field(min_length=1, max_length=255)
relation_name: str = Field(min_length=1, max_length=255)
schema_name: str = Field(
min_length=1,
max_length=255,
pattern=r"^[^\x00]+$",
)
relation_name: str = Field(
min_length=1,
max_length=255,
pattern=r"^[^\x00]+$",
)
body: str = Field(min_length=1, max_length=10_000)


Expand Down Expand Up @@ -302,7 +314,11 @@ class DbmlConvertOut(BaseModel):
class ApiKeyCreateIn(BaseModel):
"""Request body for creating an API key."""

key_name: str = Field(min_length=1, max_length=128)
key_name: str = Field(
min_length=1,
max_length=128,
pattern=r"^[^\x00-\x1F\x7F]+$",
)


class ApiKeyOut(BaseModel):
Expand Down
38 changes: 38 additions & 0 deletions backend/tests/test_schema_control_characters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from __future__ import annotations

import pytest
from pydantic import BaseModel, ValidationError

from app.schemas import ApiKeyCreateIn, DiagramViewCreateIn, TableAnnotationUpsertIn


@pytest.mark.parametrize(
("model_cls", "field_name", "other_fields"),
[
(DiagramViewCreateIn, "name", {"layout_json": {}}),
(ApiKeyCreateIn, "key_name", {}),
],
)
@pytest.mark.parametrize("control", ["\x00", "\n", "\r", "\t", "\x1b", "\x7f"])
def test_product_label_fields_reject_ascii_control_characters(
model_cls: type[BaseModel],
field_name: str,
other_fields: dict[str, object],
control: str,
) -> None:
payload = {**other_fields, field_name: f"safe{control}name"}

with pytest.raises(ValidationError):
model_cls.model_validate(payload)


def test_table_annotation_body_keeps_multiline_content() -> None:
body = "First line\nSecond line\twith indentation"

annotation = TableAnnotationUpsertIn(
schema_name="public",
relation_name="orders",
body=body,
)

assert annotation.body == body
34 changes: 34 additions & 0 deletions backend/tests/test_table_annotation_postgres_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from __future__ import annotations

import pytest
from pydantic import ValidationError

from app.schemas import TableAnnotationUpsertIn


def test_table_annotation_preserves_postgres_quoted_identifiers() -> None:
valid_schema = "my\nschema\tname"
valid_relation = "my\nrelation\tname"
body = "Test body"

annotation = TableAnnotationUpsertIn(
schema_name=valid_schema,
relation_name=valid_relation,
body=body,
)

assert annotation.schema_name == valid_schema
assert annotation.relation_name == valid_relation


@pytest.mark.parametrize("field_name", ["schema_name", "relation_name"])
def test_table_annotation_rejects_postgres_nul_identifier(field_name: str) -> None:
payload = {
"schema_name": "public",
"relation_name": "orders",
"body": "Test body",
}
payload[field_name] = "invalid\x00identifier"

with pytest.raises(ValidationError):
TableAnnotationUpsertIn.model_validate(payload)
44 changes: 44 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Product / Technical Gap Baseline

기준일: 2026-09-04

이 문서는 `pg-erd-cloud`의 코드와 운영 증거에서 확인되는 상용화 Gap만 추적한다. 구현되지 않은 기능을 완료된 것처럼 기록하지 않는다. ERD 프로젝트·스키마·다이어그램·주석·API 키에 관한 도메인 truth는 이 저장소가 소유하고, 조직 공통 CI·보안·릴리스 정책은 `ContextualWisdomLab/.github`의 released contract를 따른다.

## 현재 경계

- Backend: FastAPI + SQLAlchemy/Alembic 기반의 ERD/스키마 관리 API.
- Frontend: TypeScript/Vite.
- 배포: Docker Compose 및 Traefik 경계.
- 외부 공통 책임은 소스 복제로 들여오지 않고 versioned owner contract로 소비한다.

## Code-current Gap

### G-SEC-001 — 식별자 제어 문자 검증

`DiagramViewCreateIn.name`, `ApiKeyCreateIn.key_name`은 제품 수준의 라벨로 간주되어 ASCII C0 제어 문자와 DEL을 거부한다. 데이터베이스 식별자 도메인인 `TableAnnotationUpsertIn.schema_name`, `TableAnnotationUpsertIn.relation_name`은 PostgreSQL quoted identifier 계약에 맞춰 NUL만 거부하고 LF/TAB을 포함한 나머지 문자를 보존한다. `TableAnnotationUpsertIn.body`도 멀티라인 콘텐츠를 그대로 보존한다. 데이터베이스 식별자에 제품 라벨용 blanket C0/DEL 필터를 적용하지 않는다.

- production 계약: `backend/app/schemas.py`의 두 데이터베이스 식별자 필드는 `^[^\x00]+$`로 NUL만 거부한다.
- 회귀 증거: `test_table_annotation_postgres_identity.py`가 LF/TAB 보존과 `schema_name`·`relation_name` 각각의 NUL 거부를 검증한다. `test_schema_control_characters.py`는 제품 라벨의 별도 제어 문자 정책과 annotation body의 멀티라인 보존을 검증한다.
- 표준 근거: PostgreSQL Global Development Group. (2026). *PostgreSQL 19 documentation: 4.1. Lexical structure*. https://www.postgresql.org/docs/19/sql-syntax-lexical.html — quoted identifier는 code zero를 제외한 문자를 허용한다.
- 상태: source/test 계약은 일치했다. exact-head backend test/lint/security가 실제 runner에서 terminal GREEN이어야 병합 조건을 충족한다. queued 상태는 성공 증거가 아니다.

### G-CONFIG-001 — 런타임 secret/config KV 전환

현재 저장소 지침이 명시하듯 `backend/app/settings.py`의 `BaseSettings` 기반 환경변수 직접 로딩은 조직의 런타임 KV/credential-registry 경계에 대한 알려진 편차다. `app_secret`, database URL, LLM/OIDC/Valkey 자격정보는 환경변수를 런타임 source of truth로 사용하지 않고 bootstrap 단계에서 KV에 적재한 뒤 애플리케이션은 KV만 읽도록 이관해야 한다.

완료 조건은 다음과 같다.

- bootstrap transport와 runtime read 경계를 분리한다.
- secret 값은 로그·trace·exception에 남지 않는다.
- tenant/credential lookup 실패가 fail-closed 한다.
- 기존 환경변수 직접 읽기를 제거하는 테스트와 migration/rollback 절차가 있다.

### G-REL-001 — immutable release 부재

2026-09-03 live GitHub Releases 조회 결과 canonical release가 0개다. 상용 배포 완료를 주장하려면 protected head에서 version/CHANGELOG/tag/package를 일치시키고 SBOM, provenance, 재현성 및 rollback 증거를 포함한 immutable release를 실제 발행해야 한다.

완료 조건은 release artifact가 source SHA와 추적 가능하고, consumer가 mutable branch/head가 아니라 그 release/version을 사용하며, rollback rehearsal이 동일 artifact identity를 기준으로 재현되는 것이다.

## 현재 병합 판단

제어 문자 hardening은 database identity와 product label을 분리한 production contract와 focused regression test를 갖췄다. required exact-head workflow가 terminal GREEN이 될 때까지 merge-ready로 간주하지 않는다. 조직 runner/CodeQL control-plane 장애는 leaf 저장소의 gate 완화나 no-op commit으로 우회하지 않고 `.github` owner path에서 복구한다.
Loading