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 を使うときは、反復回数にご注意を。
