Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
c8576ae
🛡️ Sentinel: [MEDIUM] Pydantic 문자열 필드 제어 문자 검증 추가
seonghobae Aug 1, 2026
da304e4
🛡️ Sentinel: [MEDIUM] Pydantic 문자열 필드 제어 문자 검증 추가
seonghobae Aug 3, 2026
c646204
Merge branch 'main' into security/harden-pydantic-strings-22055657641…
opencode-agent[bot] Aug 3, 2026
5cbb5ac
test: cover control characters at every string position
seonghobae Aug 3, 2026
7fbe5af
🛡️ Sentinel: [MEDIUM] Pydantic 문자열 필드 제어 문자 검증 추가
seonghobae Aug 3, 2026
6b14cd5
chore(frontend): restore dependency files to main baseline
seonghobae Aug 3, 2026
7417ccd
Merge branch 'main' into security/harden-pydantic-strings-22055657641…
seonghobae Aug 4, 2026
b001b7a
Merge branch 'main' into security/harden-pydantic-strings-22055657641…
opencode-agent[bot] Aug 4, 2026
efd3cda
🛡️ Sentinel: [MEDIUM] Pydantic 문자열 필드 제어 문자 검증 추가
seonghobae Aug 4, 2026
35da03b
Merge branch 'main' into security/harden-pydantic-strings-22055657641…
opencode-agent[bot] Aug 4, 2026
0a35203
chore(scope): remove unrelated frontend drift
seonghobae Aug 4, 2026
8271a3e
chore(security): remove transient agent journal change
seonghobae Aug 6, 2026
0860f64
docs(security): define printable identifier boundary
seonghobae Aug 7, 2026
bba7c46
🛡️ Sentinel: [MEDIUM] Pydantic 문자열 필드 제어 문자 검증 추가
seonghobae Aug 7, 2026
4ae31d2
🛡️ Sentinel: [MEDIUM] Pydantic 문자열 필드 제어 문자 검증 추가
seonghobae Aug 7, 2026
9523901
🛡️ Sentinel: [MEDIUM] Pydantic 문자열 필드 제어 문자 검증 추가
seonghobae Aug 7, 2026
8b5538e
chore: narrow pull request scope
seonghobae Aug 9, 2026
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.
## 2026-08-01 - Hardening Unprotected Pydantic String Fields Against Control Characters
**Vulnerability:** Several Pydantic string fields (`DiagramViewCreateIn.name`, `TableAnnotationUpsertIn.schema_name`, `TableAnnotationUpsertIn.relation_name`, `ApiKeyCreateIn.key_name`) were missing the `pattern` parameter to restrict ASCII control characters.
**Learning:** Even when some fields have validation, developers often miss applying the same rigorous rules to new schemas. If a field lacks strict regex filtering (e.g., `pattern=r"^[^\x00-\x1F\x7F]+$"`), attackers can inject newlines, carriage returns, or terminal escapes which can lead to log forging or unexpected parsing errors down the line.
**Prevention:** Systematically apply `pattern=r"^[^\x00-\x1F\x7F]+$"` to all new identifier or name-based string fields in `backend/app/schemas.py`.
12 changes: 8 additions & 4 deletions backend/app/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ 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 +214,12 @@ 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]+$"
)
relation_name: str = Field(
min_length=1, max_length=255, pattern=r"^[^\x00-\x1F\x7F]+$"
)
body: str = Field(min_length=1, max_length=10_000)


Expand Down Expand Up @@ -302,7 +306,7 @@ 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
67 changes: 66 additions & 1 deletion backend/tests/test_schema_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@
import pytest
from pydantic import ValidationError

from app.schemas import ConnectionCreateIn, ProjectCreateIn, ProjectMemberAddIn
from app.schemas import (
ApiKeyCreateIn,
ConnectionCreateIn,
DiagramViewCreateIn,
ProjectCreateIn,
ProjectMemberAddIn,
TableAnnotationUpsertIn,
)


def test_project_name_length_is_bounded() -> None:
Expand Down Expand Up @@ -37,3 +44,61 @@ 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")


@pytest.mark.parametrize(
"valid_input",
[
"Valid Name",
"한글 이름",
"データベース",
"🚀 Project",
"name_with_underscores",
"name-with-dashes",
],
)
def test_hardened_pydantic_strings_accept_valid_input(valid_input: str) -> None:
Comment thread
seonghobae marked this conversation as resolved.
"""Accept realistic printable names in every hardened request schema."""
DiagramViewCreateIn(name=valid_input, layout_json={})
TableAnnotationUpsertIn(
schema_name=valid_input, relation_name=valid_input, body="body"
)
ApiKeyCreateIn(key_name=valid_input)


@pytest.mark.parametrize(
"control_char",
[chr(i) for i in range(32)] + [chr(127)],
)
@pytest.mark.parametrize("position_fmt", ["{}suffix", "pre{}post", "prefix{}"])
def test_hardened_pydantic_strings_reject_control_characters(
control_char: str, position_fmt: str
) -> None:
"""Reject ASCII controls and DEL at every position in hardened names."""
test_str = position_fmt.format(control_char)

with pytest.raises(ValidationError):
DiagramViewCreateIn(name=test_str, layout_json={})

with pytest.raises(ValidationError):
TableAnnotationUpsertIn(
schema_name=test_str, relation_name="valid", body="body"
)

with pytest.raises(ValidationError):
TableAnnotationUpsertIn(
schema_name="valid", relation_name=test_str, body="body"
)

with pytest.raises(ValidationError):
ApiKeyCreateIn(key_name=test_str)


def test_table_annotation_body_allows_multiline() -> None:
"""Ensure the body field is untouched by the strict validation."""
multiline_body = "Line 1\nLine 2\r\nLine 3\t(with tab)"
TableAnnotationUpsertIn(
schema_name="public",
relation_name="users",
body=multiline_body,
)
Loading