2026/10/05AI業務効率化
セキュリティ非エンジニア向け

Math.randomは危険|鍵に使うと破られる実例と直し方

Math.randomは危険|鍵に使うと破られる実例と直し方

「AIに書かせたコードに Math.random() が入っていた。これは直さないとまずいのか」——自分でアプリを組み始めた人が、一度は手を止める場所です。

結論から言うと、直すべきなのは鍵・セッション・トークン・パスワード再設定リンクに使われている分だけです。アニメーションの揺らぎや画面内のid生成に使われている分は、放置して構いません。危険かどうかは「乱数の種類」ではなく「その値が何を守っているか」で決まります。

株式会社Fyveは、AIに書かせたコードを公開前に別のAIで検査する工程を日常の運用に組み込んでいます。この記事では、2026年9月末に実例が出た CVE-2026-61500(Rejetto HFS のセッション偽造・CVSS 9.3)を教材に、私たちが自分の手元を実際に数えた結果と、今日15分でできる棚卸しの手順まで書きます。

結論:直すのは「鍵に使っている分」だけ

Math.random() は「予測できる乱数」です。暗号用に作られていないので、出てきた数字を十分な回数観測すると、次に出る数字も前に出た数字も計算できます。ただしこれが実害になるのは、その値が秘密を担っているときだけです。

用途で3つに仕分けると、判断は一瞬で終わります。

用途

例

判定

① 演出・ばらつき

アニメーションの揺らぎ、パーティクルの初期位置、デモ用のダミーデータ、配列のシャッフル

放置可

② 画面内のid

DOM要素のkey、リスト項目の一時id、ログの連番補助

原則放置可(ただしそのidがURLの一部になり、他人に見られると困る場合は③扱い)

③ 秘密を担う値

セッションID、Cookieの署名鍵、APIトークン、パスワード再設定リンク、招待コード、クーポンコード、ワンタイムパスワード

🔴 即直す

③だけを crypto.randomUUID() や crypto.randomBytes() といった暗号用の乱数に置き換えれば終わりです。①②まで一斉に置換する必要はありません。「全部 Math.random() を消そう」と動くと、触る範囲が無駄に広がって、かえって壊します。

ここまでが答えです。以下は「なぜ③だけが致命的になるのか」を、実際に攻撃された事例で確かめていきます。この事例には、乱数の話を超えた教訓が2つ入っています。

実例:Math.randomで作った鍵が、12回のログイン試行で割られた

何が起きたのか

CVE-2026-61500 は、ファイル共有サーバー Rejetto HFS(HTTP File Server)の脆弱性です。GitHub Advisory Database の記載では深刻度 CVSS 9.3(Critical)、分類は CWE-338(暗号学的に弱い擬似乱数生成器の使用)。影響を受けるのは 3.0.0 から 3.2.0 までで、3.2.1 で修正済みです。

HFS 3系はTypeScriptで書き直されており、Webフレームワークの Koa を使っています。問題は、セッションCookieに署名するための鍵を Math.random() から作っていたことです。

それだけなら「理論上まずい」で止まる話でした。実際に攻撃が成立したのは、同じ生成器から出た生の数値が、ログイン処理のなかで未認証のクライアントに返っていたからです。Cookieの中身はbase64エンコードされたJSONなので、攻撃者は自分に発行されたCookieを覗くだけで、生の乱数をそのまま読めました。

攻撃の流れ

Horizon3.ai が2026年9月30日に公開した技術解説には、攻撃の全手順が載っています。要約すると5段です。

CVE-2026-61500の攻撃フロー:ログイン12回から管理者権限とコード実行まで到達する5段階
  1. 管理者アカウントの存在を確認する
  2. 未認証で叩けるエンドポイントを12回呼ぶ——これで Math.random() の出力が12個手に入る
  3. 集めた12個を Z3 に食わせて、生成器の内部状態を復元する(Z3 はマイクロソフトが公開しているSMTソルバー=条件式を自動で解くツール)
  4. 復元した状態を逆向きに巻き戻して、プロセス起動時に作られた署名鍵を割り出す
  5. その鍵で管理者のセッションCookieを偽造し、HFSの設定機能(任意のJavaScriptを実行できるカスタムエンドポイント)からサーバー上でコードを実行する

ログインを12回試すだけで、管理者権限とリモートコード実行まで到達します。パスワードを1回も推測していないのが、この攻撃の嫌なところです。

なぜ12個で割れるのか

Node.jsのJavaScriptエンジン(V8)の Math.random() は xorshift128+ というアルゴリズムで動いています。速い代わりに、出力が可逆です。十分な数の出力を観測すれば内部状態が特定でき、内部状態が分かれば、その生成器がこれから出す数も、すでに出した数もすべて計算できます。

「鍵は起動時に1回だけ作ったから、外からは見えない」という直感は、ここで崩れます。あとから出た12個の数字から、起動時に消費された数字まで巻き戻せるからです。

芯①:弱い乱数「だけ」では破られない

この事例でいちばん大事なのは、2つの欠陥が組み合わさって初めて攻撃が成立したという点です。

欠陥

単独だと

A. 弱い乱数で鍵を作っている

攻撃者は観測する材料を持てない。理論上の弱点にとどまる

B. 生の乱数が外に返っている

ただの意味のない数字が漏れているだけ。単独では被害にならない

🔴 A + B

管理者権限の奪取とリモートコード実行

つまり自分のコードを見るときは、2点セットで見る必要があります。

  • Math.random() で秘密の値を作っていないか(弱さ)
  • 同じ生成器から出た生の値を、外に返していないか(漏れ)

「漏れ」の経路は、Cookieだけではありません。APIのJSONレスポンスにデバッグ用のフィールドが残っている、エラーメッセージに内部の値が混ざっている、クライアント側のログに出している、画面のHTMLに data- 属性として埋まっている——AIに書かせたコードでは、この手の「とりあえず返しておく」が残りやすい場所です。

私たちがコードを検査するときに、乱数の行だけを見ずにその値が最終的にどこへ出ていくかを追うのは、この型を何度も踏んでいるからです。

芯②:直し方は7月13日からあった。攻撃は「攻め方の公開」の翌日に来た

この事例の時系列は、乱数の話よりも実務に効きます。

日付

何が起きたか

2026年7月13日

CVE-2026-61500 が公開される。修正版 3.2.1 が出ており、直せる状態になっていた

7月〜9月(約2か月半)

目立った攻撃は観測されていない。静かな期間

2026年9月30日

Horizon3.ai が攻撃の全手順を技術解説として公開。前後して第三者による実証コードも公開される

その直後

VulnCheck のおとり環境(Canary)に対する探索が観測される。中国の通信事業者の単一IPから、日本と米国の環境を狙っていた

⚠️ 観測が始まった日は、報道によって10月1日・2日・3日と記述が揺れています。私たちは一次情報で1日を特定できなかったため、本記事では日付を断定していません。確かなのは「解説公開から数日のうちに来た」という点です。

ここから引ける結論は1つです。

CVE-2026-61500の時系列:修正版は7月13日に出ていたが、攻撃は9月30日の技術解説公開の数日後に来た

対応の期限は「修正版が出た日」ではなく「攻め方が公開された日」です。 修正版が出てから約2か月半、攻撃は来ませんでした。来たのは、誰でも再現できる手順が世に出た直後です。

これは順番が逆になりやすい話です。「CVEが出たから急いで上げる」のは正しいのですが、現実に手が回らず後回しにしたとき、カウントダウンが始まるのは解説記事や実証コードが出た瞬間になります。そしてそのタイミングは、こちらからは見えません。だから「出たときに上げる」しかない、という話に戻ってきます。

自分が使っている部品について、いま確認できることは2つだけです。

  • 使っている部品(ライブラリ・ミドルウェア・自前で建てたサーバーソフト)の版を、いま言えるか
  • その版に対して出ているCVEから、何日経っているか

見つけたのはAIだった——攻める側の棚卸しが速くなっている

この脆弱性は、人が総当たりで探したものではありません。Horizon3.ai の研究者が、Anthropic の限定アクセスプログラム「Project Glasswing」(同社は2026年7月に参加)を通じて、脆弱性探索向けのモデル Mythos を使って見つけています。

私たちが注目したのは、見つけた「速さ」ではなく「繋げ方」です。Horizon3.ai の解説には、こう書かれています。

Mythos は、追加の指示を出さなくても、この理論上の問題を実証可能なものに変えた「別経路のPRNG漏洩」を自力で見つけた(原文: "It did not require follow-on prompting to find the disparate PRNG leak that made this theoretical issue a demonstrable one.")

先ほどの「A+B」の構造を思い出してください。AIは①弱い乱数で鍵を作っている箇所と、②まったく別のコードパスで生の乱数が漏れている箇所を見つけ、その2つを1本の鎖として繋いだうえで、「漏れている観測量は状態復元にちょうど足りる」と判断しています。Z3 を使う方針も、ソルバーの実装まで含めてモデル側が出しています。

人間のコードレビューが見落としやすいのは、まさにここです。Math.random() の行を見れば「暗号用じゃないですね」とは言えます。しかし別ファイルのログイン処理が生の値を返していることまで同時に視野に入れ、「この2つが揃うと管理者になれる」と結論するには、コード全体を同時に見る必要があります。

この事実には、防御側にとって裏返しの意味があります。同じことを自分のコードに対してやれるということです。やり方は後述します。

モデルそのものの位置づけや、「能力だけを配る」という提供形態については、こちらの記事で整理しています。

Claude Mythos 5とは|モデルを渡さず能力だけ配る

自分の手元を数えてみた:27箇所のうち、鍵に使っていたのは0件

記事を書く前に、私たちは自分の手元にあるJavaScript/TypeScriptのコードを機械的に数えました。他人の事例を解説するだけでは、読者が自分のコードに対して何をすべきか分からないからです。

結果はこうです。

対象

Math.random() の箇所数

自分で書いた(AIに書かせた)コードのみ

27箇所/4ファイル

外部から取り込んだ配布物も含めた全体

85箇所/51ファイル

そして27箇所の用途を1つずつ目で見た内訳が、次のとおりです。

用途

箇所

判定

演出・ばらつき(アニメーション、パーティクル、カメラの揺れ、出題の分散、配列シャッフル)

24

放置可

画面内・ログ内のid生成(日付+ランダム文字列 形式)

3

原則放置可

🔴 鍵・セッション・トークン・再設定リンク

0

——

0件でした。ただしこれは「気をつけていたから」ではなく、私たちが書いているものの大半が、認証を持たないスクリプトと社内向けの道具だからです。ログイン機能を持つアプリを作っていれば、同じ結果にはなりません。胸を張れる数字ではなく、「自分の構成だとどこに出るか」が分かった数字として扱っています。

もう1つ分かったことがあります。85-27=58箇所は、自分が書いていないコードでした。計測タグ、polyfill、スクレイプして取り込んだ配布物などです。これらの用途は検品していないので危険とも安全とも言えません。ただ「自分のコードを直せば終わり」ではない、という事実だけは残ります。

この数え方は、読者がそのまま再現できます。次節がその手順です。

今日15分でできる棚卸し5手順

手順1:数える

自分のプロジェクトの中で、Math.random() が何箇所あるかを数えます。ripgrep を使う場合はこうです。

rg -n --glob '*.{js,ts,jsx,tsx,mjs,cjs}' --glob '!**/node_modules/**' 'Math\.random\(\)'

grep しかない環境なら、こちらで足ります。

grep -rn --include="*.js" --include="*.ts" --include="*.jsx" --include="*.tsx" "Math.random()" . | grep -v node_modules

PythonやPHPを使っているなら、同じ発想で random.random() random.randint rand() mt_rand() を探します。「暗号用ではない乱数」はどの言語にもあります。

手順2:用途で3つに仕分ける

出てきた行を、冒頭の表どおりに①演出/②画面内のid/③秘密を担う値に分けます。ここで手が止まる行だけが、本当の仕事です。 私たちの場合、27行のうち判断に迷ったのは3行だけでした。

迷ったときの質問は1つです。「この値を他人が当てられたら、何ができてしまうか」。何も起きないなら①か②、何かができてしまうなら③です。

手順3:生の値が外に出ていないか見る

③に分類した行について、その値(および同じ生成器から出た他の値)が外に返っていないかを追います。見る場所は4つです。

  • APIのレスポンスJSON(デバッグ用フィールドの残骸)
  • Cookieの中身(base64は暗号化ではありません。誰でも中を読めます)
  • エラーメッセージ・スタックトレース
  • HTMLの data- 属性、クライアント側のコンソールログ

CVE-2026-61500 が実害になったのは、この3手順目に該当する漏れがあったからです。③が1件でもあるなら、ここは飛ばせません。

手順4:③だけを置き換える

暗号用の乱数に差し替えます。言語ごとの対応は次のとおりです。

環境

使わない

使う

Node.js

Math.random()

crypto.randomUUID() / crypto.randomBytes(32)

ブラウザ

Math.random()

crypto.getRandomValues() / crypto.randomUUID()

Python

random モジュール

secrets.token_urlsafe() / secrets.token_hex()

PHP

rand() / mt_rand()

random_bytes() / bin2hex(random_bytes(32))

加えて、署名鍵やセッションの秘密は、コードで生成せず環境変数から読むのが基本です。CVE-2026-61500 も、鍵を外から渡していれば乱数の弱さが鍵に届きませんでした。

手順5:使っている部品の版を数える

最後は自分のコードの外です。使っている部品の版を書き出し、CVEが出ていないかを確認します。npm audit や pip-audit のような仕組みを定期的に走らせているなら、ここは機械に任せられます。

この手順を5番目に置いたのは、優先度が低いからではありません。自分のコードの乱数より、自分が建てたサーバーソフトの版のほうが、実際には危険だからです。CVE-2026-61500 で狙われたのは、自作アプリではなく「インストールして使うファイル共有サーバー」でした。

AIに書かせるときに、指示を1行足しておく

ここまでは出てきたものを直す話でした。最初から出させないほうが安いので、AIに実装を頼むときの指示に1行足しておきます。

私たちが使っている書き方はこうです。

ID・トークン・セッション・署名鍵・パスワード再設定リンクなど、他人に推測されると困る値には Math.random() を使わないこと。暗号論的に安全な乱数(crypto.randomUUID() / crypto.randomBytes())を使い、鍵は環境変数から読むこと。演出やUIのidは Math.random() でよい。

「Math.random() を禁止」だけ書くと、AIは演出のコードまで crypto に置き換えて読みにくくします。禁止ではなく、用途の線引きを渡すのが要点です。

そのうえで、私たちは書いた本人とは別のプロセスのAIに、公開前のコードと文章を検査させる運用を取っています。同じ文脈を持ったままの自己チェックは、自分が書いた前提をそのまま信じてしまうため、「AとBが揃っている」型の指摘にいちばん弱いからです。別プロセスに分けるだけで、見える欠陥の種類が変わります。

AIにコードを検査させる具体的な進め方は、こちらで実例つきで解説しています。

AIでコードをセキュリティ監査する方法|Claude実践事例
AI業務効率化AIでコードをセキュリティ監査する方法|Claude実践事例

非エンジニアがAIで開発を進めるときの全体像(ツール選定からセキュリティの考え方まで)は、こちらにまとめています。

バイブコーディング完全ガイド|非エンジニアがAI開発するためのツール・セキュリティまで【2026年最新】
Claude Codeバイブコーディング完全ガイド|非エンジニアがAI開発するためのツール・セキュリティまで【2026年最新】

よくある誤解を3つ

「Math.randomは全部ダメなのでは」

違います。アニメーションの揺らぎを crypto.getRandomValues() で作る必要はありません。Math.random() が速いのは設計どおりで、演出やシャッフルはむしろ適任です。問題は配置場所だけです。

「UUIDを使っているから安全では」

UUIDという形式は安全性を保証しません。Math.random() で組み立てたUUID風の文字列は、見た目はUUIDでも予測できます。実際、ライブラリを入れずにUUIDを自作するコードはよく出回っており、その中身が Math.random() であることは珍しくありません。使うなら crypto.randomUUID() です。

「社内ツールだから関係ない」

CVE-2026-61500 が刺さったのは、まさに「社内のファイル共有に使う小さなサーバー」です。そして観測された探索は、日本の環境も対象に入っていました。社内向けに作ったものがインターネットに面しているかどうかは、作った本人の意図ではなく、置き方で決まります。

まとめ

  • Math.random() を直すのは③秘密を担う値に使っている分だけ。演出とUIのidは放置可
  • CVE-2026-61500(CVSS 9.3・HFS 3.0.0〜3.2.0・3.2.1で修正)は、弱い乱数と生の値の漏洩が組み合わさって管理者権限の奪取まで到達した
  • だから2点セットで見る——秘密に弱い乱数を使っていないか/生の値を外に返していないか
  • 対応の期限は修正版が出た日ではなく、攻め方が公開された日。修正版から約2か月半は静かで、解説公開の数日後に探索が来た
  • 見つけたのはAIで、価値は速さではなく離れた2つの欠陥を1本の鎖として繋いだこと。同じことは防御側もできる
  • 自分の手元を数えたら27箇所・鍵に使っていたのは0件。数えれば15分で終わる作業です

株式会社Fyveは、AIに実装させた成果物を公開前に別のAIで検査する工程を、日々の運用として回しています。AIに書かせる量が増えるほど、効くのは「書き方の上手さ」より出てきたものを機械的に数えて仕分ける手順でした。まずは Math.random() を数えるところから試してみてください。

参考にした情報源

※ CVE-2026-61500 の深刻度・影響範囲は2026年10月6日時点の情報です。最新の状況は上記の一次情報をご確認ください。

AIを使う会社と、使わない会社。
その差は、開き始めています。

ここ数年でAIは急速に進化し、正しく導入できている企業とそうでない企業とでは、業務効率や人件費に大きな差が生まれ始めています。「AI導入に興味はあるが、実際に何ができて、どこから手をつければいいか分からない」——そんな方は、まずこの無料プレゼントに目を通してみてください。

無料プレゼント:様々な業種にAIを導入して分かった、成功の型と失敗パターン ― 無料でダウンロードする
← 記事一覧に戻る