diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 1c145b3a0..6912ffe19 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -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-23 - Hardening Pydantic String Fields Against Control Characters (Part 2) +**Vulnerability:** Additional user-provided string fields (`DiagramViewCreateIn.name`, `TableAnnotationUpsertIn.schema_name`, `TableAnnotationUpsertIn.relation_name`, `ApiKeyCreateIn.key_name`) 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. Furthermore, adding explicit `# SECURITY: ...` comments alongside the validation helps future developers understand the necessity of the regex pattern. +**Prevention:** Use explicit regex validation `pattern=r'^[^\x00-\x1F\x7F]+$'` on all Pydantic string fields to strictly reject control characters, and document the reason with inline security comments. diff --git a/backend/app/schemas.py b/backend/app/schemas.py index d7c6de77d..6b7177a60 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -190,7 +190,8 @@ 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]+$") + # SECURITY: Prevent control character injection (e.g. CRLF, Null Byte) # Opaque client layout (node positions, hidden tables, viewport). The API # bounds the serialized size in the endpoint to prevent abuse. layout_json: dict @@ -214,8 +215,10 @@ 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-\x1F\x7F]+$") + # SECURITY: Prevent control character injection (e.g. CRLF, Null Byte) + relation_name: str = Field(min_length=1, max_length=255, pattern=r"^[^\x00-\x1F\x7F]+$") + # SECURITY: Prevent control character injection (e.g. CRLF, Null Byte) body: str = Field(min_length=1, max_length=10_000) @@ -302,7 +305,8 @@ 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]+$") + # SECURITY: Prevent control character injection (e.g. CRLF, Null Byte) class ApiKeyOut(BaseModel): diff --git a/backend/tests/test_schema_validation.py b/backend/tests/test_schema_validation.py index 317292b86..183114985 100644 --- a/backend/tests/test_schema_validation.py +++ b/backend/tests/test_schema_validation.py @@ -37,3 +37,27 @@ def test_conn_name_rejects_control_characters() -> None: ConnectionCreateIn(conn_name="my\x00conn", dsn="postgresql://localhost/db") with pytest.raises(ValidationError): ConnectionCreateIn(conn_name="my\nconn", dsn="postgresql://localhost/db") + +from app.schemas import ApiKeyCreateIn, DiagramViewCreateIn, TableAnnotationUpsertIn + +def test_diagram_view_create_in_rejects_control_characters() -> None: + with pytest.raises(ValidationError): + DiagramViewCreateIn(name="View\nNewline", layout_json={}) + with pytest.raises(ValidationError): + DiagramViewCreateIn(name="View\x00Null", layout_json={}) + +def test_table_annotation_upsert_in_rejects_control_characters() -> None: + with pytest.raises(ValidationError): + TableAnnotationUpsertIn( + schema_name="schema\n", relation_name="valid_table", body="some body" + ) + with pytest.raises(ValidationError): + TableAnnotationUpsertIn( + schema_name="valid_schema", relation_name="table\x00", body="some body" + ) + +def test_api_key_create_in_rejects_control_characters() -> None: + with pytest.raises(ValidationError): + ApiKeyCreateIn(key_name="Key\nNewline") + with pytest.raises(ValidationError): + ApiKeyCreateIn(key_name="Key\x00Null")