Skip to content
Merged
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
44 changes: 42 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ print(sensitive_data) # logging モジュールを使う
- **セキュリティヘッダーをミドルウェアで付与**(X-Content-Type-Options, X-Frame-Options, etc.)
- **SQL はパラメータ化クエリのみ**。文字列フォーマット禁止
- **ファイルパスは `pathlib.Path` で操作**し、パストラバーサルを防ぐ
- **XML 処理には `defusedxml` を使用**。標準の `xml.etree.ElementTree` は XXE・展開爆弾に脆弱(FT180 で確認)
```bash
uv add defusedxml
```
`import xml.etree.ElementTree` の代わりに `import defusedxml.ElementTree` を使う。

### 依存関係の脆弱性スキャン

Expand Down Expand Up @@ -233,6 +238,37 @@ AI エージェント(Claude 等)がこのコードベースを正確に理
- `nene2.http.problem_details_response()` で RFC 9457 エラー応答
- `nene2.http.PaginationQueryParser` でページネーション

### APIRouter パターン(必須)

すべての FastAPI アプリで `APIRouter` + `create_app()` ファクトリパターンを使うこと。

```python
# ✅ 正しい構造 — app.py
router = APIRouter()

@router.post("/items") # ← すべてのルート定義は router に紐付ける
def create_item(...): ...

@router.get("/items/{item_id}")
def get_item(...): ...

def create_app() -> FastAPI: # ← create_app() はファイル末尾に定義する
application = FastAPI(title="...")
application.include_router(router)
return application

app = create_app() # ← モジュールレベルの app は最終行
```

**`create_app()` はファイルの末尾**(全 `@router.xxx()` デコレーター定義の後)に置くこと。
先に `app = create_app()` を呼ぶと `router` にルートが登録される前に `include_router()` が実行され、
エンドポイントが空になるバグが発生する(FT182 で発見)。

- `router = APIRouter()` → ファイル先頭の定数・モデル定義の後
- `@router.post(...)` デコレーター → ハンドラー関数の定義
- `create_app()` → ファイル末尾
- `app = create_app()` → ファイル最終行

### ミドルウェアスタック順序(重要)

`app.add_middleware()` は **LIFO**(後から追加したものが外側になる)。
Expand Down Expand Up @@ -378,7 +414,11 @@ Python 標準ライブラリ・サードパーティライブラリを nene2-pyt
テンプレート: docs/templates/field-trial-report.md
6. DX Review(6ペルソナ)を実施(後述)
7. FT番号が3の倍数なら セキュリティ診断 を実施(後述)
8. Follow-up Issues を GitHub Issue に変換
8. Follow-up Issues をその場で修正してからクローズする
- 発見した問題(摩擦点・セキュリティ指摘)は FT PR に含めて修正する
- 修正 → テスト全通過 → PR に含める → GitHub Issue は PR 内でクローズ(Closes #NNN)
- CLAUDE.md 追記・docs 更新・サンドボックスのコード修正すべてを同じ PR に含める
- 「外部依存の修正待ち」など対応不可能な理由がある場合のみ Issue を残し、理由を PR 説明に記載する
9. まとめて main merge → パッチバージョン(v1.8.N)でリリース
```

Expand Down Expand Up @@ -418,7 +458,7 @@ Python 標準ライブラリ・サードパーティライブラリを nene2-pyt

**合否判定**:
- **合格**: 全カテゴリ問題なし
- **条件付き合格**: MEDIUM 以下の指摘のみ、次 FT までに修正
- **条件付き合格**: MEDIUM 以下の指摘のみ → **同 FT の PR 内で修正してからマージ**
- **不合格**: HIGH/CRITICAL の指摘あり → main merge 前に必須修正

---
Expand Down
61 changes: 61 additions & 0 deletions docs/how-to/decimal-unicode-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# How-to: decimal モジュールと Unicode 数字入力

## Python の Decimal は Unicode 全角数字を受け入れる

`decimal.Decimal()` は Unicode の全角数字(U+FF10〜U+FF19: 0123456789)を
そのまま数値として解釈します。HTTP API を通じてユーザーが全角数字を入力した場合、
**Pydantic の `str` フィールドを通過してしまう**ことがあります。

```python
from decimal import Decimal

Decimal("123") # → Decimal('123') ← 正常に変換される
Decimal("1.5") # → Decimal('1.5')
```

## 問題: 想定外の入力が通過する可能性

金融計算 API で `price: str = Field(...)` としている場合、
クライアントが `"1000"` を送ると `Decimal("1000")` → `Decimal('1000')` として処理されます。
これ自体はエラーではありませんが、**入力の正規化が必要な場合**(ログ記録・DB 保存等)は
Unicode 正規化を行ってから処理してください。

```python
import unicodedata
from decimal import Decimal

def parse_decimal_safe(value: str) -> Decimal:
"""Unicode 正規化(NFKC)して Decimal に変換する."""
normalized = unicodedata.normalize("NFKC", value.strip())
return Decimal(normalized)
```

`unicodedata.normalize("NFKC", ...)` は全角数字を半角に変換します。

```python
unicodedata.normalize("NFKC", "123") # → "123"
unicodedata.normalize("NFKC", "1.5") # → "1.5"
```

## Pydantic でのバリデーション

Pydantic の `model_validator` を使って入力値の正規化を強制することを推奨します。

```python
from pydantic import BaseModel, Field, model_validator

class PriceRequest(BaseModel):
price: str = Field(max_length=20, description="価格(半角数字)")

@model_validator(mode="before")
@classmethod
def normalize_unicode(cls, values: dict) -> dict:
import unicodedata
if "price" in values and isinstance(values["price"], str):
values["price"] = unicodedata.normalize("NFKC", values["price"])
return values
```

## 関連 Issue

- [FT176] #500: parse_decimal_safe() の Unicode 全角数字受け入れ挙動を文書化
57 changes: 57 additions & 0 deletions docs/how-to/email-address-parsing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# How-to: メールアドレスのパースと parseaddr() の挙動

## parseaddr() は寛容なパーサー

`email.utils.parseaddr()` は RFC 2822 準拠のフォーマット(`"Name <addr@example.com>"` 形式)を解析しますが、
**不正なアドレスを渡してもエラーを送出せず、空文字列を返します**。

```python
from email.utils import parseaddr

# 正常ケース
parseaddr("Alice <alice@example.com>") # → ("Alice", "alice@example.com")
parseaddr("alice@example.com") # → ("", "alice@example.com")

# 不正なアドレス — エラーにならず ("", "") を返す
parseaddr("not-an-email") # → ("", "")
parseaddr("") # → ("", "")
parseaddr("bad @ format") # → ("", "")
```

## HTTP 境界での検証は別途行うこと

`parseaddr()` の戻り値が空かどうかで有効性を確認しても、
**セキュリティ上の検証としては不十分**です。ユーザーが入力したアドレスは
Pydantic の `EmailStr` や正規表現で検証した後に `parseaddr()` を使ってください。

```python
import re
from email.utils import parseaddr

_EMAIL_RE = re.compile(r"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$")

def validate_and_parse(raw: str) -> tuple[str, str] | None:
name, addr = parseaddr(raw)
if not addr or not _EMAIL_RE.match(addr):
return None
return name, addr
```

## ヘッダーインジェクション対策

`Subject` や `From` ヘッダーに CR/LF (`\r\n`) が含まれると
**メールヘッダーインジェクション**が発生します。`email.message.EmailMessage` を使えば
自動的にエスケープされますが、`smtplib.sendmail()` に生文字列を渡す場合は
事前に CR/LF を除去してください。

```python
import re
_INJECT_RE = re.compile(r"[\r\n]")

def sanitize_header(value: str) -> str:
return _INJECT_RE.sub("", value)
```

## 関連 Issue

- [FT182] #511: parseaddr() の寛容な挙動を How-to ドキュメントに記載
Loading