大文字と小文字を区別するミドルウェアのパス

大文字と小文字の扱いの不整合

説明

Expressでミドルウェアのパスを RegExp にすると、既定では大文字と小文字を区別します。セキュリティ用ミドルウェアだけが区別し、実際のルートが区別しない場合、エンドポイントの文字の大小を変えてミドルウェアを回避できることがあります。たとえば /admin の保護が /Admin や /AdMiN に適用されない場合です。

想定される影響

  • 管理ページや保護されたAPIの認証・認可の回避
  • ミドルウェアが一致しないことによるログや監査記録の欠落
  • リクエスト回数制限、CSRF対策、CSPやHSTSなどのヘッダーの適用漏れ
  • 同じと考えていたパスに異なる保護が適用されることによるアクセス制御の誤り

対処方法

  • 実際のルートが大文字と小文字を区別しない場合は、保護用ミドルウェアの正規表現にも i フラグを付けてください。例は /^\/admin(?:\/|$)/i や new RegExp('^/admin(?:/|$)', 'i') です。
  • 可能なら app.use('/admin', authMiddleware) のような文字列パスを使い、Expressの既定の区別しない動作に合わせてください。
  • フレームワークの設定を確認してください。既定ではルーティングの大文字・小文字の区別は無効です。正規表現と文字列のパスを混在させる場合は、その違いを記録してテストしてください。
  • 可能なら保護用ミドルウェアを全体に適用し、例外だけを限定的に許可してください。
  • /admin、/Admin、/AdMiN などの表記で、保護を回避できないことを確認してください。

例

変更前

javascript
const express = require("express");
const app = express();

function requireAdmin(req, res, next) {
  if (!req.user || !req.user.isAdmin) return res.sendStatus(403);
  next();
}

// 変更前: 'i'フラグがなく、大文字と小文字を区別します。
app.use(/^\/admin\/api\/.*$/, requireAdmin);

app.get("/admin/api/users", (req, res) => {
  res.send("users");
});

// GET /Admin/api/usersはrequireAdminを経由せずに到達します。

変更後

javascript
const express = require("express");
const app = express();

function requireAdmin(req, res, next) {
  if (!req.user || !req.user.isAdmin) return res.sendStatus(403);
  next();
}

// 方法1: 文字列パスは既定で大文字と小文字を区別しません。
app.use("/admin", requireAdmin);

// 方法2: 正規表現が必要なら'i'フラグを付けます。
// app.use(/^\/admin(?:\/|$)/i, requireAdmin);

app.get("/admin/api/users", (req, res) => {
  res.send("users");
});

説明:

  • 変更前: i がないため、/admin/... は保護しますが /Admin/... は一致しません。ミドルウェアは文字の大小を区別し、文字列ルートはExpressの既定で区別しないため、差が生じます。
  • 変更後: 文字列パスで同じ既定の照合動作を使うか、正規表現に i を付けます。この不整合によるミドルウェアの回避を防ぎます。

参考資料