Skip to content

security: Request::getFile() empty-tmp_name guard + RedirectGuard open-redirect allowlist (#408)#414

Merged
hideyukiMORI merged 1 commit into
mainfrom
security/408-getfile-and-redirect
May 22, 2026
Merged

security: Request::getFile() empty-tmp_name guard + RedirectGuard open-redirect allowlist (#408)#414
hideyukiMORI merged 1 commit into
mainfrom
security/408-getfile-and-redirect

Conversation

@hideyukiMORI
Copy link
Copy Markdown
Owner

評価レポート #401 § 4 Security の 2 件を fail-closed で対応。

Request::getFile() defense-in-depth

`$_FILES[$key]` の structure を前段で validate:

  1. canonical 5 keys (name/type/tmp_name/error/size) 全存在を要求
  2. `error === UPLOAD_ERR_NO_FILE` で null (semantic "no file")

UPLOAD_ERR_NO_FILE 以外の error は従来通り UploadedFile 生成 → controller が validate() 経由で error code を surface 可能 (情報非破壊)。

`Nene\Xion\RedirectGuard` (新規)

`ControllerBase::location($uri, false)` の open-redirect surface を fail-closed allowlist で守る:

  • env `NENE_ALLOWED_EXTERNAL_REDIRECTS` が空 / 未設定 = 全 external host 拒否 (HTTP 403)
  • comma-separated host allowlist、大文字小文字 ignore、trim semantics
  • `isExternalAllowed()` (predicate) + `ensureExternalAllowed()` (throw on deny) + `allowedHosts()` 全 public で test 容易

`location()` の $flag=true 経路 (internal redirect) は不変、$flag=false 経路のみ guard 経由に。

Tests

  • `RequestGetFileGuardTest` (6 cases): malformed entry / NO_FILE / 他 error / real upload
  • `RedirectGuardTest` (8 cases): deny default / allow / case-insensitive / no-host / throw / trim

Compose / env

`NENE_ALLOWED_EXTERNAL_REDIRECTS` (default 空) + production-deployment.md matrix 1 行。

Test plan

  • composer test 113/113 (99 → 113、+14 新)
  • composer test:http 24/24 (1 expected skip)
  • composer analyze (Phan) exit 0
  • composer format:check exit 0

Closes #408.

…n-redirect allowlist (#408)

評価レポート (#401 § 4 Security) 指摘の 2 件を解消。

### Request::getFile() 強化

\`\$_FILES[\$key]\` の structure validation を前段で追加 (defense in depth):

1. canonical 5 keys (name/type/tmp_name/error/size) すべて存在を要求
   → 任意 key 欠落で null
2. \`error === UPLOAD_ERR_NO_FILE\` の場合は null
   → 「ファイル未選択」の semantic を getFile レベルで吸収

UPLOAD_ERR_NO_FILE 以外の error (INI_SIZE, PARTIAL, NO_TMP_DIR 等) は
従来通り UploadedFile を生成 → controller は validate() 経由で error
コードを surface できる (情報を消さない)。

### Nene\Xion\RedirectGuard (新規)

\`ControllerBase::location(\$uri, false)\` の open-redirect surface を
fail-closed allowlist で守る:

- \`isExternalAllowed(string \$uri): bool\` (predicate、test 容易)
- \`ensureExternalAllowed(string \$uri): void\` (throw on deny)
- \`allowedHosts(): array\` (env 解析公開)
- Default policy: env (NENE_ALLOWED_EXTERNAL_REDIRECTS) 空 or 未設定で
  全 external host を拒否 → controller が user-supplied URL を素通し
  しても phishing 誘導にならない
- comma-separated host allowlist、大文字小文字を ignore、trim する

ControllerBase::location() を \$flag=false 経路で
\`RedirectGuard::ensureExternalAllowed()\` 呼出しに切替。\$flag=true
経路 (internal redirect under URI_ROOT) は不変。

### Tests

- tests/Unit/Xion/RequestGetFileGuardTest.php (6 cases): 4 deny + 2 pass
- tests/Unit/Xion/RedirectGuardTest.php (8 cases): allowlist / deny /
  case-insensitive / no-host / throw / trim semantics

### Compose / env / docs

- compose.yaml に NENE_ALLOWED_EXTERNAL_REDIRECTS (default 空)
- docs/development/production-deployment.md env matrix に 1 行追加
  (fail-closed の挙動 + 推奨 value pattern を明記)

### Verification

- composer test 113/113 (99 既存 + 6 getFile + 8 RedirectGuard)
- composer test:http 24/24 (1 expected skip)
- composer analyze (Phan) exit 0
- composer format:check exit 0

Closes #408.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hideyukiMORI hideyukiMORI merged commit 56a4064 into main May 22, 2026
2 checks passed
@hideyukiMORI hideyukiMORI deleted the security/408-getfile-and-redirect branch May 22, 2026 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: Request::getFile() empty tmp_name guard + location() open-redirect guard (eval report PR #401)

1 participant