/ #Cloudflare #TypeScript

Cloudflare Workers の PBKDF2 は本番だけ反復 100,000 回まで

テストは全部通っていたのに

個人で作っている Web サービスの API を Cloudflare Workers に載せています。 本番にデプロイして最初のアカウントを作ろうとしたところ、サインアップが 500 エラーになりました。 ログに出ていたのはこれです:

Pbkdf2 failed: iteration counts above 100000 are not supported (requested 210000).

Workers には bcrypt を載せず、Web Crypto の PBKDF2 でパスワードをハッシュにしていました。 反復回数は 210,000 回です:

const ITERATIONS = 210_000

const key = await crypto.subtle.importKey(
  'raw',
  new TextEncoder().encode(password),
  'PBKDF2',
  false,
  ['deriveBits'],
)
const bits = await crypto.subtle.deriveBits(
  { name: 'PBKDF2', hash: 'SHA-256', salt, iterations: ITERATIONS },
  key,
  256,
)

本番の Workers では、PBKDF2 の反復回数に 100,000 回の上限があります。 それを超えると例外になります。

なぜ手元では気づけないのか

wrangler dev も、テストに使っている @cloudflare/vitest-pool-workers も、オープンソース版のランタイム workerd の上で動きます。 workerd では、この上限が初期状態で外されています(workerd#1471)。 上限が残っているのは Cloudflare の本番環境だけです。

そのため、

  • ローカルの動作確認: 通る
  • テスト: 全部通る
  • 本番: 最初のサインアップで 500

となりました。 利用者が 0 人のうちに気づけたのが救いです。

直し方

反復回数を 100,000 に下げました:

// 本番の Workers は 100,000 を超えると例外を投げる。
// ローカルの workerd とテストは上限を掛けないので、超えても気づけない。上げないこと。
export const ITERATIONS = 100_000

本番の制約は手元で再現できないので、値そのものをテストで固定しました:

it('反復回数が本番の Workers の上限(100,000)を超えない', () => {
  expect(ITERATIONS).toBeLessThanOrEqual(100_000)
})

210,000 のままのコードで、このテストが落ちることも確かめています。

回数はハッシュと一緒に保存しておくべきだった

反復回数を変えると、それまでに作ったハッシュは検証できなくなります。 今回は回数をコードに直接書いていたので、ローカルで作ったアカウントは作り直しになりました。

Django のように、回数をハッシュと一緒に保存しておけば、

pbkdf2_sha256$100000$<salt>$<hash>

古いハッシュは古い回数で検証できます。 回数を上げても、ログインのついでに新しい回数で作り直せばよくなります。

100,000 回で大丈夫なのか

正直、十分とは言えません。 OWASP のパスワード保存のチートシートでは、PBKDF2-HMAC-SHA256 の反復回数として 600,000 回を推奨しています。 Workers の Web Crypto だけでは、この推奨値に届きません。

この上限については 2023 年から「低すぎる」という Issue が出ています(workerd#1346)。 2026 年 10 月時点では、上限を 1,000,000 に上げる Pull Request も出ています(workerd#7550、まだマージされていません)。 上限が上がったときに回数を上げられるよう、回数はハッシュと一緒に保存しておくのがおすすめです。

「ローカルとテストで通る = 本番で動く」とは限りません。 Workers で PBKDF2 を使うときは、反復回数にご注意を。