タグ: セキュリティ

  • 動かしていなかったから、漏れていなかっただけ

    動かしていなかったから、漏れていなかっただけ

    問い合わせフォームにボット対策を入れようとして、先に別のことが分かりました。

    いまの作りでは、ブラウザ側にどんな対策を入れても効きません。

    この記事で扱うこと

    送信先のURLが配信されるJavaScriptに入る作りになっていた話と、その直し方です。あわせて、画像の文字を読ませる方式を選ばなかった理由も書きます。

    先頭の6文字が、置き場所を決めていた

    このサイトはNext.jsで作っています。環境変数の名前には、決まりがあります。

    名前 どこから見えるか
    CONTACT_ENDPOINT サーバーだけ
    NEXT_PUBLIC_CONTACT_ENDPOINT ブラウザにも配られる

    名前の先頭に NEXT_PUBLIC_ が付いていると、その値は組み立てのときにJavaScriptの中へ埋め込まれます。訪問者に配られるファイルの一部になる、ということです。

    フォームはブラウザの中で動く部品なので、送信先を知る必要がありました。だから素直に NEXT_PUBLIC_ を付けていました。付けなければ読めないので、それ以外に選びようが無いように見えていました。

    鍵の掛かっていない扉が、隣に並ぶ

    この形だと、送信先のURLはフォームを開かなくても分かります。配られたファイルを開けば書いてあります。

    フォームを通さずに送れる

    見えない入力欄を足しても、送信までの時間を測っても、それらはフォームを使った場合にだけ効きます。

    送信先を直接叩く相手には、そもそも通り道が違うので関係がありません。鍵を掛けた扉の横に、鍵の掛かっていない扉が並んでいる状態でした。

    動かしていなかったから、漏れていなかった

    ここで確認したところ、本番の環境変数は設定されていませんでした。

    フォームは、送信先が無いときは「お問い合わせ先は準備中です」と出す作りにしてあります。つまり公開されてからずっと、フォームは表示されておらず、URLもどこにも出ていませんでした。

    安全だったのではなく、使っていなかっただけ

    結果として何も漏れていませんが、設定を入れた瞬間に漏れる形のまま置いてありました。

    この種の問題は「まだ起きていない」と「起きない」の区別が付きにくく、動かし始めた日にまとめて効いてきます。

    ついでに言うと、フォームが出ていなかったこと自体も問題でした。問い合わせ先として案内していた場所が、ずっと準備中だったということになります。

    自分のサイトに窓口を作る

    直し方は、間に1枚挟むことでした。

    前 後
    送信先 ブラウザ → 外部サービス ブラウザ → 自分のサイト → 外部サービス
    URLの在り処 配られるJavaScript サーバーだけが持つ
    判定の場所 ブラウザ(迂回できる) サーバー(迂回できない)

    ブラウザから見えるのは /api/contact だけになります。ここを直接叩くことはできますが、叩いた先で何を見ているかは分かりません。

    画像の文字を読ませる方式は、入れなかった

    ボット対策というと、ゆがんだ文字や、信号機の写真を選ばせるものが思い浮かびます。今回は入れていません。

    読みにくい文字は、機械より先に人が脱落する

    問い合わせは、そもそも件数が多くありません。1日に何百件も来るものではない。

    それに対して、1件取りこぼす損のほうが大きいと考えました。迷惑な送信を100件止める価値より、相談しようとした人が1人あきらめる損のほうが重い、という判断です。

    人がやらないことを、3つ見る

    代わりに置いたのは、訪問者の手間が一切増えない3つです。

    見ること 人がやらない理由
    見えない欄が埋まっている 画面に出ていないので、触りようがない
    開いてすぐ送っている 3秒で文章は書けない
    短時間に何度も送っている 同じ相談を10分に4回はしない

    どれも、正しく使っている人には何も起きません。止めるために何かをさせるのではなく、やっていないことを確認する形にしています。

    見えない欄の名前を、慎重に決めた

    ひとつだけ、注意が必要な部分があります。見えない欄の名前です。

    この欄を name や url のような一般的な名前にすると、ブラウザやパスワード管理ソフトの自動入力が、画面に出ていなくても勝手に埋めることがあります。

    人の送信が、黙って捨てられる

    この欄が埋まっている送信は、機械とみなして捨てます。しかも成功したように見せて捨てます。理由を返すと、次からその欄を避ければ通ると分かってしまうからです。

    つまり自動入力に埋められた人は、送信したつもりで、何も届いていない状態になります。これがこの仕組みの一番の失敗です。

    自動入力が狙わない名前にしたうえで、キーボードの移動先からも読み上げの対象からも外しています。

    回数の数え方には、限界がある

    正直に書いておくと、短時間の回数を数える部分は完全ではありません。

    このサイトのサーバー側の処理は、必要なときに起動して、しばらくすると消えます。数えた記録は共有されず、消えるときに一緒に無くなります。厳密には数えられません。

    それでも、続けて叩いてくる相手は同じ実体に当たりやすいので、一番困る「短時間に何百件」は止まります。正確さが要るようになったら、外に記録を置くことになります。いまの流量でそこまで持つ必要は無いと判断しました。

    今回の学び

    まず、名前の先頭6文字が、置き場所を決めていたということ。NEXT_PUBLIC_ は便利な省略ではなく、「これは配る」という宣言でした。付けなければ読めないから付ける、では順番が逆でした。

    次に、ブラウザ側の判定は、通り道が1本のときにだけ効くこと。もう1本、鍵の掛からない道が並んでいれば、そちらが使われます。対策を足す前に、道が何本あるかを数える必要がありました。

    そして、止めるために手間を増やさないこと。問い合わせのように件数が少ないものでは、止めた数より取りこぼした数のほうが重くなります。人がやらないことを確認するだけなら、正しく使う人には何も起きませんでした。

  • 勉強法(6) 効いたのは、技術そのものではなかった

    勉強法(6) 効いたのは、技術そのものではなかった

    この連載の最後に、身も蓋もない話を書きます。

    仕事につながった場面を振り返ると、効いていたのは技術そのものではありませんでした。

    この記事で扱うこと

    実際に役に立った3つ。どれも技術の名前ではなく、技術の扱い方の話です。このサイトで実際にやったことと対応させて書きます。

    1. 説明できること

    「なぜそうしたのか」を聞かれて答えられるかどうか。ここでいちばん差が出ました。

    動くものを作れる人は多い。ところが「その方法を選んだ理由」になると、急に答えが揃わなくなります。

    このサイトで言えば、同意の仕組みがそうでした。先に読み込んでおいて送信の可否だけ切り替える方法と、同意まで読み込まない方法がある。後者を選んだ理由は、前者だと自分のコードを全部読んでも「送っていない」と言い切れないからです。

    説明できる形を選ぶ、という選び方がある

    この判断は、技術的な優劣で決めていません。あとで説明を求められたときに答えられるかで決めています。

    仕事だと、この視点が要る場面のほうが多い。「動きます」で終わらせられる相手ばかりではありません。

    2. 引き継げること

    自分で書いていないコードを直す仕事が続いた時期があります。書いた人はもういない。仕様書もない。あるのは動いているコードだけ。

    そのとき欲しかったのは、常に判断の理由でした。

    コードから分かること コードから分からないこと
    何をしているか なぜその方法を選んだか
    どういう順番で動くか なぜこの順番でなければならないか
    この値を除外していること 除外しないと何が起きるのか

    右の列が分からないと、触っていいかどうかが判断できません。一見無駄に見える処理が、過去の不具合への対処だったということは珍しくない。理由が書いていないと、消して初めて分かります。

    いま自分が書くコードには、処理の説明ではなく理由を書いています。引き継ぐ側に回った経験が、そのまま書き方に効きました。

    3. 止まらないこと

    3つめは、壊れたときにどうなるかを先に決めておくことです。

    このサイトのCMSは共用サーバーにあって、混むと応答しません。最初はそれで公開作業ごと止まっていました。

    直したのは速度ではなく、失敗する前提で書き直したことです。

    場面 決めておいたこと
    CMSが500を返す 間を置いて投げ直す。ただし要求が違う場合は投げ直さない
    記事が取れない 0件のフィードを返す。エラーにしない
    同意の記録が読めない 「同意していない」扱いにする

    どれも「うまくいく道」ではなく、うまくいかなかったときにどちらへ倒れるかの話です。

    止まらない作りは、能力ではなく事前の判断

    特別な技術は使っていません。分からないときにどちらへ倒すかを、書く時点で決めただけです。

    決めていないと、そのときの都合で倒れます。都合で倒れたものは、あとから説明できません。

    3つに共通していたこと

    並べてみると、全部「あとから来る人」への態度でした。

    誰のためか
    説明できること 判断を求める相手
    引き継げること 次に触る人
    止まらないこと 壊れたときに困る人

    3つとも、書いている自分には直接の得がありません。書いた時点では、誰も困っていないからです。

    効いてくるのは、いつも後からでした。そして後から効いたものだけが、次の仕事につながっていました。

    技術が要らない、という話ではない

    誤解のないように書いておくと、土台としての技術は要ります。読めないコードは引き継げないし、仕組みを知らなければ壊れ方も予想できません。

    ただ、そこは足りていないと何も始まらないが、足りたからといって差がつく場所ではないという感覚です。差がついていたのは、同じものを作れる人どうしの間でした。

    今回の学び

    まず、動かせる人は多いということ。差がついていたのは動くかどうかではなく、なぜそうしたかを説明できるかどうかでした。

    次に、引き継ぐ側の経験が、書き方を変えること。読めないコードに困った経験がないと、理由を残す動機が湧きません。困った側に一度回ると、書き方が変わります。

    そして、効いてくるのは全部あとからだということ。3つとも、書いた時点では誰も困っていません。得が見えないうちにやっておいたものだけが、後で効きました。

  • Criticalが4件並んだ日にやったこと

    Criticalが4件並んだ日にやったこと

    朝、通知を見たらCriticalが4件並んでいました。どれも同じ相手、Next.js本体です。

    この日にやったことを順に書きます。結論から言うと、上げました。ただ、上げる前にやったことのほうが本題です。

    この記事で扱うこと

    脆弱性の通知が来たときに、何をどの順で確かめたか。自分のサイトが実際に影響を受けるのかを切り分ける手順と、それでも上げた理由です。

    まず、中身を読む

    4件は、実は2種類でした。同じものが2つのファイルで検出されているので、件数が倍に見えていました。使っているものを書いた一覧と、実際に入った版を固定した一覧の2つです。

    種類 内容
    A Windowsで動かしているサーバーでの、認証不要の遠隔コード実行
    B 画像最適化の機能で、AVIF形式を扱ったときの遠隔コード実行

    どちらも「認証不要」で「遠隔からコードを実行される」。言葉としては、これ以上ないくらい重い部類です。

    Aは、条件に当てはまらなかった

    Aの説明には、条件がはっきり書いてありました。WindowsのファイルシステムでNext.jsのサーバーが動いている場合に成立する、と。

    そして、LinuxとmacOSは影響を受けないと明記されていました。

    このサイトの本番はVercelで動いています。Linuxです。開発しているのもMacです。どちらも対象外でした。

    「Critical」は自分への影響度ではない

    重大度は、その脆弱性が最悪の場合にどれだけひどいかを表しています。自分の環境で成立するかどうかは、また別です。

    色と数字だけ見て慌てると、条件を読まずに動くことになります。読めば1分でした。

    Bは、入口の広さを自分で確かめた

    Bのほうは、条件が「AVIF形式の画像を最適化したとき」です。攻撃者が用意した画像を最適化させられると成立します。

    このサイトは画像の最適化機能を使っています。ここで確かめるのは、「誰でも好きな画像を最適化させられる状態か」です。

    設定を見ると、最適化を許可している場所が2つだけ指定してありました。

    remotePatterns: [
      { hostname: "localhost", port: "8080", ... },
      { hostname: "cms.kisaku.site",
        pathname: "/wp/wp-content/uploads/**" },
    ]

    つまり、CMSの画像置き場にあるファイルしか最適化できません。任意のURLを渡しても弾かれます。

    そこに置くには、CMSにログインして画像をアップロードする必要があります。入口はかなり狭いと言えます。

    「狭い」は「無い」ではない

    ここで止めたくなりますが、狭いだけで塞がってはいません。

    CMSのログインが破られれば成立します。そしてCMSは共用サーバー上のWordPressで、攻撃対象としてはありふれた構成です。「入口が狭いから大丈夫」は、入口が破られない前提に乗っています。

    結局、上げた

    Aは対象外、Bは入口が狭い。それでも上げました。

    理由は単純で、上げない理由のほうが薄かったからです。

    上げる 上げない
    作業量 修正版への更新1回 0
    壊れる可能性 細かい版の更新なので低い —
    放置した場合 — 入口が破られた時点で成立する

    大きな作り替えを伴う更新なら、慎重になる理由があります。今回は末尾の数字が1つ上がるだけの修正版で、その中身も「問題のある機能を止める」というものでした。

    本番に入るものと、そうでないものを分けて数える

    更新したあと、残りを確かめました。ここで1つ、見方を変えています。

    そのまま数えると12件残っていました。ところが「本番に配るものだけ」で数え直すと0件です。

    npm audit --omit=dev
    found 0 vulnerabilities

    残っていた12件は、全部開発中にしか動かない道具のものでした。型を生成するツールが使っている、さらにその中のライブラリです。

    配られないものは、訪問者に届かない

    これらはビルドのときに自分のマシンで動くだけで、配信されるファイルには入りません。訪問者のブラウザにも、サーバーにも届きません。

    0件にはしたいのですが、直すには型生成ツールを大きく更新する必要があり、そちらのほうが壊れる可能性が高い。今回は見送って、理由を書き残しました。

    数字を0にすることが目的になると、危険度の低いもののために動いているものを壊します。分けて数えると、どちらを急ぐかが決まります。

    やったことをまとめると

    順番 やったこと かかった時間
    1 4件が実は2種類だと確認 1分
    2 条件を読んで、対象外を1つ外した 2分
    3 残りの入口の広さを設定で確かめた 5分
    4 更新して、型検査を通した 5分
    5 本番に入るものだけで数え直した 1分

    手を動かしたのは4だけで、残りは読んで確かめる時間でした。

    今回の学び

    まず、重大度は「最悪の場合」の話であって、自分への影響ではないということ。条件が書いてあるので、読めば当てはまるかどうかが分かります。今回は片方が最初から対象外でした。

    次に、「入口が狭い」で止めないこと。狭いのは事実でしたが、狭さはこちらの設定に依存しています。設定を変えた日に広がるかもしれません。狭いことを理由に放置すると、その理由を覚えている人がいなくなった時点で危なくなります。

    そして、数える範囲を先に決めること。全部で12件と、配信されるものの中で0件では、判断がまったく変わります。件数を減らすこと自体を目的にすると、動いているものを壊しにいくことになります。

  • ソース解説(3) 旗を折るのではなく、読み込まない

    ソース解説(3) 旗を折るのではなく、読み込まない

    クッキー同意のバナーは、置くこと自体は難しくありません。難しいのは、「同意していない人には何も送っていない」と自分で言い切れる形にすることでした。

    今回はその部分のコードを読んでいきます。保存する形、版番号を持たせた理由、そして「読み込まない」という選び方です。

    この記事で扱うこと

    同意の記録をどう保存しているか、壊れた記録をどう扱うか、離れた部品へどうやって変化を伝えているか、そしてJavaScriptが動かない環境で何をしないことにしたかです。

    止めているのは1つだけ

    最初に決めたのは、何に対して同意を求めるかです。

    このサイトが読み込むものを数えると、外部へ通信するのはタグマネージャー1つでした。書体は自前で配信しているので提供元への通信は起きません。紹介リンクは、押して移動したときに初めて相手へ届きます。問い合わせの送信先は、送信ボタンを押したときだけ呼ばれます。

    項目を増やすほど、選んでもらえなくなる

    読了状態と明暗の設定も保存していますが、こちらはサイトの中で完結していて外へ出ません。止める理由が無いので、同意の対象に入れていません。

    並べるのは、実際に止まるものだけにしています。止まらないものを並べると、選ぶ手間だけが増えます。

    保存する形

    記録はこの形です。

    export type ConsentRecord = {
      analytics: boolean;
      decidedAt: string;
      version: number;
    };

    保存先はCookieではありません。サーバー側がこの値を使わないからです。Cookieはリクエストのたびに自動で送られますが、送り先に使い道がありません。ブラウザの中だけに置いています。

    decidedAt を持っているのは、あとから「いつ同意したか」を示せるようにするためです。

    版番号を持たせた理由

    version が、この設計でいちばん考えた部分です。

    const VERSION = 1;

    何に同意を求めるかが変わったら、この数字を上げます。版が違う記録は「まだ答えていない」として扱い、もう一度たずねます。

    古い同意を、新しい送信先に流用できない

    同意は「そのとき説明されたものに対して」もらっています。

    あとから送信先を1つ足したときに、以前の同意をそのまま使い回すと、その人が知らない相手へ黙って送り始めることになります。聞き直す手間より、そちらのほうが問題として大きい。

    版番号は、その判断を忘れても実行される形にしたものです。

    壊れている記録は、無かったことにする

    読み出す側は、自分で書いた値でも疑っています。

    const parsed: unknown = JSON.parse(raw);
    if (typeof parsed !== "object" || parsed === null) return null;
    
    const record = parsed as Partial<ConsentRecord>;
    if (typeof record.analytics !== "boolean") return null;
    if (record.version !== VERSION) return null;

    型を unknown で受けているのが要点です。保存されている文字列が、こちらの期待する形である保証はありません。古い形式が残っていることも、別のタブで書き換わっていることもあります。

    全体が try で囲んであり、保存が使えない環境では例外が出ます。そのときも null、つまり「同意していない」扱いになります。

    壊れたときに、どちらへ倒れるか

    プライベートモードなどで保存が読めないと、答えが分かりません。

    分からないときに送る側へ倒すか、送らない側へ倒すか。ここでは送らない側に倒しています。判断がつかない状態を「同意あり」として扱う理由がありません。

    書き込む側も同じ考え方です。保存に失敗しても、そのページのあいだは選んだとおりに動かします。記録が残らないので次に開いたときはまたたずねますが、「保存できないから同意も無かったことにする」よりは筋が通っています。

    離れた部品へ、どう伝えるか

    記録するのはバナー、それを見て読み込むかどうか決めるのはタグマネージャー側の部品です。2つは画面上の別の場所にあり、親子の関係もありません。

    保存の仕組みには「変わったよ」と教えてくれる機能がないので、自分で合図を投げています。

    window.dispatchEvent(
      new CustomEvent<ConsentRecord>(CONSENT_CHANGED, { detail: record }),
    );

    受け取る側は、こう待っています。

    useEffect(() => {
      const sync = () => setGranted(readConsent()?.analytics === true);
      sync();
      window.addEventListener(CONSENT_CHANGED, sync);
      return () => window.removeEventListener(CONSENT_CHANGED, sync);
    }, []);

    最初に1回読んで、そのあとは合図が来るたびに読み直します。最後の return は後片付けで、これを書かないと画面を移動するたびに待ち受けが増え続けます。

    旗を折るのではなく、読み込まない

    同意の実装には2通りあります。先に読み込んでおいて「送ってよいか」の旗だけ切り替えるやり方と、同意までそもそも読み込まないやり方です。

    ここでは後者にしました。

    if (!GTM_ID || !granted) return null;

    この1行で、同意が無いあいだは何も出力されません。

    前者だと、自分のコードでは確かめられない

    先に読み込む方式では、同意していない人のブラウザにもタグの本体が届いています。何を送る・送らないの判断が相手側の設定に移るので、こちらのコードを全部読んでも「同意前は何も送っていない」とは言い切れません。

    読み込まなければ、通信そのものが起きません。確かめたいことと、確かめられることが一致します。

    代償として、同意しなかった人の分は数字に出ません。それは正しく「同意しなかった人がいる」という事実なので、受け入れています。

    JavaScriptが動かない環境で、何をやめたか

    タグマネージャーには、JavaScriptが無効な環境向けに noscript の中へ枠を1つ置く決まりがあります。それを置くのをやめました。

    同意は、ブラウザに保存した記録を読んで判断しています。JavaScriptが動かない環境では、その記録を読むことも、同意をたずねることもできません。それでも枠だけは動きます。

    置いたままにすると、「同意を確かめられない人にだけ、確かめずに送る」ことになります。分からないときに送らない側へ倒す、という前提をここでも通しました。

    今回の学び

    まず、同意は対象とセットでしか意味を持たないということ。何に対する同意かが変われば、前の同意は使えません。版番号は、その判断を人の記憶に頼らず実行させるための仕掛けでした。

    次に、分からないときにどちらへ倒れるかを、先に決めておくこと。読めない・保存できない・確かめられない、という状態は必ず起きます。そのとき何が起きるかは、書いた時点で決まっています。

    そして、自分のコードで説明できる形を選ぶこと。旗を切り替える方式でも、おそらく実際には送られません。ただ、それは相手の設定を信じているだけで、こちらからは確かめられない。言い切れるかどうかで選びました。

  • 「送っていない」と言い切るために、同意まで読み込まないことにした

    「送っていない」と言い切るために、同意まで読み込まないことにした

    クッキーの同意を取る仕組みを入れました。作りながら一番考えたのは、見た目や文言ではなく、「同意していない状態」をどう表すかでした。

    この記事で扱うこと

    止める対象の数え方、旗を立てる方式と読み込まない方式の違い、JavaScriptが無い環境の扱い、そして同意の記録に版を持たせた理由です。法律の解釈ではなく、作りの話です。

    止める対象を、先に数えた

    最初にやったのは、ページを開いたときに外部へ通信するものを数えることでした。

    送り先 いつ通信するか 同意の対象か
    タグマネージャー(解析) ページを開いた瞬間 対象
    アフィリエイトのリンク先 押して移動したときだけ 対象外
    問い合わせの送信先 送信ボタンを押したときだけ 対象外
    書体 自前で配信しているので通信しない 対象外

    結果、止める必要があるのは1つだけでした。読了状態と画面の明暗もブラウザに保存していますが、こちらはサイトの中で完結していて外部へは出ません。

    項目を増やすほど、選べなくなる

    同意の画面に細かい分類を並べたくなりますが、実際には止まらないものを並べても、選ぶ手間が増えるだけです。

    読んでいる途中で急に出てくる画面なので、そこに置いた選択肢の数がそのまま負担になります。数えた結果が1つなら、1つだけ出すのが正しい。

    旗を立てるか、読み込まないか

    同意していない状態の表し方には、大きく2通りあります。

    ひとつは、タグを先に読み込んでおいて「送ってよいか」の旗だけを切り替えるやり方。もうひとつは、同意までそもそも読み込まないやり方です。

    旗を切り替える 読み込まない
    同意前のブラウザ タグ本体は届いている 何も届いていない
    送る・送らないの判断 相手側の設定にもある こちらのコードだけで決まる
    確かめ方 設定を含めて追う必要がある 通信が0件かを見れば済む
    同意しなかった人の数字 推定として残ることがある 残らない

    後者を選びました。

    確かめたいことと、確かめられることを一致させる

    確かめたいのは「同意前は何も送っていない」ことです。

    旗を切り替える方式では、タグ本体は相手のブラウザに届いています。何を送るかの判断が相手側の設定に移るので、こちらのコードを最後まで読んでも、送っていないと言い切れません。設定画面まで含めて追う必要が出ます。

    読み込まなければ、通信が起きる余地そのものがありません。開発者ツールで通信の一覧を見て0件なら、それが答えになります。実際に本番で測って、同意前は0件、「同意する」を押した瞬間に読み込まれることを確認しました。

    代償として、同意しなかった人の分は数字に出ません。これは正しく「同意しなかった人がいる」という事実なので、受け入れています。

    JavaScriptが無い環境をどうするか

    タグマネージャーには、JavaScriptが無効な環境向けにnoscriptで画像枠を置く決まりがあります。これを外しました。

    同意はブラウザに保存した記録を読んで判断しています。JavaScriptが動かない環境では、その記録を読むことも、同意をたずねることもできません。それでもnoscriptの中身だけは動きます。

    置いたままにすると、同意を確かめられない人にだけ、確かめずに送ることになります。分からないときに送らない側へ倒すのがこの仕組みの前提なので、ここでも送らないほうを選びました。

    バナーは操作を止めていない

    画面の中央に出して、選ぶまで先へ進めなくする作り方もあります。採らなかったのは、止める必要が無いからです。

    先に読み込みを止めてあるので、答えないまま読み進めても何も送られません。止めなければ計測が始められない作りなら、選ばせるまで通さない意味があります。先に止めてあるなら、たずねるほうは静かでよいはずです。

    ボタンは2つとも同じ大きさにしました。片方を小さくしたりリンクのように見せたりすると、選べるように見せかけて実際には選びにくい形になります。

    同意の記録に版を持たせた

    保存している記録には、選択と日時のほかに版の番号を入れています。何に同意を求めるかが変わったら、この数字を上げます。版が違う記録は「まだ答えていない」として扱い、もう一度たずねます。

    同意は「そのとき説明されたものに対して」もらっています。あとから送信先を足したときに古い同意を流用すると、その人が知らない相手へ黙って送り始めることになります。聞き直す手間より、そちらのほうが問題が大きいと考えました。

    今回の学び

    まず、選択肢の数は、数えてから決めること。分類を先に用意すると、実際には止まらないものまで並びます。外部へ通信するものを数えたら1つだったので、聞くことも1つになりました。

    次に、「していない」を証明できる形にしておくこと。旗を切り替える方式でも、実際には送られません。ただし、送っていないことをこちらのコードだけで示せません。同じ結果でも、確かめられるかどうかは別の性質です。

    そして、分からないときにどちらへ倒すかを、先に決めておくこと。JavaScriptが無い環境では同意を確かめられません。そのときに送るのか送らないのかは、個別に迷うと毎回違う結論になります。「分からないなら送らない」と決めておけば、判断が要らなくなります。

  • 登録してから、広告を出すまで

    登録してから、広告を出すまで

    このサイトで、成果報酬型の広告を扱えるようにしました。

    手続きそのものは難しくありません。順番を間違えると手戻りになるところがいくつかあったので、そこを中心に残しておきます。

    この記事で扱うこと

    登録から掲載までの流れと、着手する順番。そして「何を紹介するか」を先に決めておくべき理由です。

    全体の流れ

    順番
    1. 掲載するサイトを整える     ← 審査で見られる
    2. ASPに登録する(サイト審査)
    3. 広告主ごとに提携を申し込む   ← 1件ずつ
    4. 承認される
    5. リンクタグを取得する
    6. 記事に置く
    7. 広告であることを表示する

    時間がかかるのは2と4です。どちらも相手の審査待ちなので、こちらからは進められません。待っている間に何をやっておくかで、承認後の速さが変わります。

    先にやっておくべきだったこと

    プライバシーポリシー

    これが最初です。広告を出すかどうかに関係なく、アクセス解析でCookieを使い、問い合わせフォームで個人情報を受け取っているなら、すでに必要になっています。

    広告の話が出てから慌てて作ることになりました。順番としては、サイトを公開した時点で用意しておくものです。

    広告であることを示す仕組み

    2023年10月から、広告を広告と分かる形で示すことが必要になりました。書き忘れると規制に触れます。

    本文に手で書く方法もありますが、手で書くものは、いつか書き忘れます。記事にタグを付けたら自動で表示が出る形にしておくと、忘れようがなくなります。

    これは提携が決まる前に作れます。実際、先に作っておいたので、承認後は記事にタグを付けるだけで済みました。

    何を紹介するかを決める

    ここがいちばん大事だと思います。「実際に使っているものだけ」と先に決めました。

    使っていないものを紹介できない理由

    このサイトは、書いた人が何を考えて作ったかを読んでもらう場所です。使ったことのないものを勧めた瞬間、他の記事も同じ調子で書かれていると思われます。

    1件の報酬と、全記事の信頼が釣り合いません。

    この条件で残るものは、多くありません。実際に契約しているサービスのうち、扱いのあるものだけになります。選択肢が減るのは、条件が効いている証拠だと考えることにしました。

    1つの広告主を、複数の記事に置くとき

    紹介できるものが少ないと、同じ広告主を複数の記事に置くことになります。ここで手を抜くと、同じ文面が何ページにも並びます。

    読者からは「どの記事にも同じ広告が挟まっている」としか見えません。検索エンジンから見ても、同一の内容が複数ページに散っている状態です。

    そこで、記事ごとに違うことを書く形にしました。企画の記事には選定の基準を、構築の記事にはつまずいた点を、公開の記事には契約前に知りたかったことを。どれもその記事で扱っている内容の延長線上にあります。

    この形なら、追記そのものが記事の情報量を増やします。

    置かない記事を決める

    候補に挙げた記事のうち1本を外しました。URLの処理に関する記事で、ドメインにもサーバーにも触れていません。

    ここに広告を置くと、内容と無関係な広告を挟んだことになります。読んだ人の信頼を削る割に、クリックもされないはずです。

    置ける場所すべてに置かない

    掲載できる場所の数と、掲載すべき場所の数は違います。文脈が合わないところに置くと、合っている場所の説得力まで下がります。

    収益の見込みについて

    正直に書くと、この規模のサイトで大きな金額にはなりません。

    それでも意味があると考えたのは、仕組みを一度通しておくと、判断の材料が手に入るからです。何がクリックされ、何がされないか。どの記事から動きがあるか。これは推測では分かりません。

    そして、こうして手順と判断を書き残せます。

    今回の学び

    まず、待ち時間のある手続きは、待つ前に準備を終えること。審査は自分では進められません。その間にできることを先に片付けておくと、承認された日に掲載できます。

    次に、条件を先に決めておくこと。「実際に使っているものだけ」を後から決めると、すでに置いたものを見直すことになります。基準は、選ぶ前に決めるから機能します。

    そして、忘れうることは、忘れられない形にすること。広告の表示は、書き忘れが規制に触れます。手で書く運用にした時点で、いつか漏れます。

    最後に、置ける場所すべてに置かないこと。数を増やすほど1件あたりの説得力は下がります。合う場所にだけ置くほうが、結果として届きます。

  • 使っていないものは、書かない

    使っていないものは、書かない

    広告を入れて収益にできないか、という話から始まりました。

    調べていて手が止まったのは、広告そのものではありません。プライバシーポリシーのページが、このサイトに無かったことです。

    この記事で扱うこと

    広告を入れるかどうかに関係なく、すでに必要だったという話。テンプレートをそのまま貼らなかった理由。そして、置く場所と管理の仕方をどう決めたかです。

    広告の話から入ったが、広告の問題ではなかった

    広告配信サービスの規約を読むと、Cookieを使うことと、利用者が無効にできることを、プライバシーポリシーに明記するよう求められています。

    そこで自分のサイトを見て、ページ自体が無いことに気づきました。

    ただ、よく考えると広告の話が出る前から必要でした。

    すでにやっていたこと 本来必要だったこと
    アクセス解析でCookieを使う 何を集めているか、断る方法の説明
    問い合わせフォームを作った 受け取った情報の使い道の説明
    読了状態をブラウザに保存 保存していること自体の説明

    広告は4つめにすぎません。

    新しいことを検討して、古い抜けが見つかる

    今回は「広告を入れるには何が要るか」を調べたことで、すでに必要だったのに無かったものが出てきました。

    抜けは、抜けているという理由では見つかりません。誰も困っていないからです。別のことを始めようとしたときに、ついでに見つかることのほうが多いと思います。

    テンプレートをそのまま貼らなかった

    この手の文書には雛形がたくさんあります。貼れば5分で終わります。

    ただ、実際に読んでみると使っていないサービスの記述が入っていました。広告配信サービスを利用しています、という一文もそのひとつです。

    これを貼ると、使っていないのに使っていると書いてある状態になります。

    書いてあることは、確かめられる

    プライバシーポリシーは、読む人が確かめるための文書です。書いてあるとおりかどうかは、ページのソースを見れば分かります。

    実態と違うことが書いてあれば、それは説明ではなく、ただの飾りです。飾りを置くくらいなら、無いほうがまだ正直かもしれません。

    そこで、現時点では広告配信を行っていないと明記しました。始めたら、そのとき追記します。

    見えないことをしているなら、書く

    もうひとつ迷ったのが、読了状態の保存です。

    これはブラウザの中だけに記録していて、こちらのサーバーには届きません。個人を特定する情報でもありません。書かなくても問題にはならない種類のものです。

    それでも書きました。理由は、利用者から見えないからです。

    記事一覧に「読了済み」と出るのは、どこかに記録されているからです。読む人がそれに気づいたとき、どこに何が保存されているのか分かる場所があるほうがいい。法的に必要かどうかと、説明したほうがいいかどうかは、別の基準です。

    置き場所と、管理の仕方

    フッターの最下部に置いた

    ナビゲーションには入れませんでした。普段見たいものではないので、並べると他の項目の邪魔になります。

    一方で、探しに来た人が見つけられないのは困ります。フッターの最下部は、この種の文書が置かれる場所として広く定着しています。決まった場所にあることが、探す側にとっての手がかりになります。

    CMSから取る形にした

    文面をコードに直接書くこともできました。やめた理由は、更新のされ方です。

    コードに書いた場合
    サービスを1つ増やす
       ↓
    ファイルを編集して、ビルドして、デプロイする
       ↓
    面倒なので後回しになる
       ↓
    実態と食い違ったまま残る

    この文書は正しさが時間とともに失われる種類のものです。使っているサービスが変われば、その日から内容がずれます。

    用語集と同じく、管理画面から直せる形にしました。手順が軽いほど、実際に更新されます。

    今回の学び

    まず、新しいことを始める前に、足元の抜けが出てくること。広告の検討をしなければ、しばらく気づかないままでした。何かを増やそうとするときは、いま動いているものの前提も一緒に点検する機会だと思います。

    次に、テンプレートは、書いてある内容が自分に当てはまるか確かめてから使うこと。雛形は考える手間を省いてくれますが、確かめる手間まで省くと、事実と違う文書ができあがります。

    そして、法的に必要かどうかと、説明したほうがいいかどうかは別だということ。見えないところで何かをしているなら、書いておく理由はそれだけで十分です。

    最後に、更新されない場所に置くと、正しさは時間とともに失われること。書いた瞬間だけ正しい文書は、しばらく経つと嘘になります。直しやすい場所に置くことは、内容を正しく保つための設計の一部です。

  • 届かない問い合わせ

    届かない問い合わせ

    問い合わせフォームを作りました。

    実装そのものは小さいのですが、決めることが3つありました。どれも「動くかどうか」ではなく「壊れたときにどう見えるか」の話でした。

    この記事で扱うこと

    メールアドレスを載せない選択、送信先が未設定のときにフォームを出すかどうか、そして失敗をどう伝えるか。共通しているのは「成功したように見える失敗」をどう防ぐかです。

    1. メールアドレスを直接載せない

    いちばん簡単なのは、ページにメールアドレスを書くことです。実装が要らず、壊れる要素もありません。

    ただし、公開した以上は自動で収集され、迷惑メールが届き始めます。個人サイトでも例外ではありません。

    そこで、フォームの送信先だけを外部サービスに任せる形にしました。受け取り用のアドレスがブラウザ側に出ません。

    方式 手間 アドレスの露出
    アドレスを載せる ゼロ する
    自前でメール送信 サーバーと送信サービスの契約が必要 しない
    外部サービスへ送信 登録して1行設定するだけ しない

    自前で用意する案もありましたが、問い合わせ1つのために管理するものが増えます。メール送信サービスの契約、鍵の管理、送信失敗時の扱い。得られるものに対して重すぎると判断しました。

    2. 送信先が未設定のとき、フォームを出すか

    ここが今回いちばん考えた点です。

    外部サービスの送信先URLは、環境変数で渡します。つまり設定を入れるまでは、送信先が空のままです。

    このとき、フォームをどう扱うか。素直に実装すると、こうなります。

    何も考えずに書くと
    フォームは表示される
       ↓
    入力して送信できる
       ↓
    送信先が空 → どこにも届かない
       ↓
    それでも「送信しました」と表示される

    これが最悪の状態です。

    送った側は、届いたと思って返信を待ちます。こちらには何も残りません。問い合わせが来なかったのか、届かなかったのかも分かりません。

    届かない問い合わせは、無いより悪い

    フォームが無ければ、相手は別の連絡手段を探すか、諦めます。どちらにしても相手は状況を把握できます。

    しかし届かないフォームは、相手に「連絡した」と誤認させます。返信が来ないことで、対応が悪いという印象だけが残ります。何もしないより損をしています。

    そこで、設定が無いときはフォームごと出さないことにしました。

    実装
    const ENDPOINT = process.env.NEXT_PUBLIC_CONTACT_ENDPOINT;
    
    if (!ENDPOINT) {
      return <p>お問い合わせ先は準備中です。</p>;
    }

    3行です。「準備中」と正直に出すほうが、動くように見えて届かないより、はるかにましです。

    3. 失敗を隠さない

    設定が正しくても、送信は失敗します。通信が切れる、外部サービスが落ちている、上限に達している。

    このとき、画面をどうするか。「送信しました」と出してしまうのは簡単です。実装が楽ですし、その場では体験が滑らかに見えます。

    しかし結果は2番目と同じで、相手は届いたと思って待ちます。

    失敗したときは失敗したと表示し、時間をおいて試すよう伝える形にしました。気まずさより、事実のほうが役に立ちます。

    3つに共通していたこと

    並べてみると、判断の軸は1つでした。

    避けたかった状態
    うまくいったように見えて、実は何も起きていない

    この状態が厄介なのは、誰も気づけないことです。エラーも出ず、画面も正常で、記録も残りません。問題があること自体が分からないので、直しようがありません。

    失敗するなら、見えるところで失敗させる

    機能が動かないことより、動かないのに動いて見えることのほうが高くつきます。「準備中です」と出ていれば、設定を入れ忘れていることに自分でも気づけます。静かに成功したふりをする実装は、自分自身も欺きます。

    今回の学び

    まず、簡単な方法には、たいてい代償があること。アドレスを直接載せるのは実装ゼロですが、迷惑メールという継続的なコストを払います。手間の総量で比べる必要があります。

    次に、未設定の状態を、設計に含めること。設定が入った状態だけを想定して作ると、入っていない期間の挙動が事故になります。「まだ設定していない」は必ず通る道です。

    そして、成功の表示は、成功したときだけ出すこと。当たり前に聞こえますが、実装を楽にするために崩れやすい原則でもあります。表示は事実に従わせるべきです。

    最後に、静かに失敗する仕組みを作らないこと。これは今回に限らず、繰り返し出てくる主題です。エラーが出る失敗は直せます。直せないのは、失敗したことが分からない失敗のほうです。

  • 上げる先が、ひとつしかなかった

    上げる先が、ひとつしかなかった

    脆弱性の警告が28件。そのうち23件が next に集約されることまでは分かっていました。あとは上げるだけ——のはずが、まず「どこまで上げるか」で手が止まりました。

    この記事で扱うこと

    更新先のバージョンをどう選んだか、自動化ツールがやってくれたこととやってくれなかったこと、そして「入れる必要のない設定」を消した判断。メジャーバージョンを2つ跨ぐ更新を、確認しながら進めた記録です。

    まず、選択肢を数える

    手元は 14.2.35 でした。自動で提案されていたのは 16.3.1。2世代ぶんの差があります。

    最初に考えたのは「まず14系の最新まで上げて、それで消える分を消そう」でした。メジャーを跨がなければ壊れにくいからです。これは実行しませんでした。調べたら、前提が違っていたからです。

    サポート状況
    Next.js 14  サポート終了(2025-10-26)
                最終パッチ 14.2.35 ← 手元にあるのがこれ
    
    Next.js 15  Maintenance LTS  2026年10月まで
    Next.js 16  Active LTS       2027年10月まで

    14系の最新は、すでに手元にありました。「上げる」という操作に余地が無く、18件の警告は14に留まる限り1件も消えません。

    やっても何も起きないコマンドを、実行しかけた

    調べる前は「14系の最新に上げましょう」と判断していました。実行すれば何かが変わるはず、という思い込みです。手を動かす前にサポート期限を1つ調べただけで、この無駄が消えました。作業の前に前提を確認するのは、遠回りに見えて速いです。

    15を選ばなかった理由

    残る選択肢は15と16です。15の方が変更が小さいので、一見こちらが安全に見えます。

    しかし15のサポートは2か月後に切れます。そこで上げても、すぐ同じ作業をやり直すことになります。「変更が小さい」ことの価値は、その変更が長持ちするかどうかで決まります。2か月で無効になるなら、小さいことに意味がありません。

    実質的に、上げる先は16しかありませんでした。

    何が壊れるかを、先に調べる

    移行ガイドを読んで、このサイトに該当するものだけを抜き出しました。全部を読むのではなく、自分のコードに関係するものを探すという読み方です。

    変更点 このサイトへの影響
    params が非同期になる 該当。記事ページで同期的に使っている
    React 19.2 が必要 該当。18.3.1 から上げる必要がある
    next lint の廃止 該当。package.json に書いてある
    画像設定の既定値変更 明示指定していないので影響なし
    Turbopackが既定に webpack設定を持っていないので影響なし
    middleware の改名 使っていない

    該当したのは3つだけでした。2世代ぶんの変更に見えて、実際に手を入れる場所は限られていると分かった時点で、作業の見通しが立ちます。

    自動化ツールは、半分やってくれる

    Next.jsには移行用のコードモッド(コードを機械的に書き換えるツール)が用意されています。2つ実行しました。

    実行したもの
    # パッケージ更新・設定ファイルの書き換え・lintの移行
    npx @next/codemod@canary upgrade latest
    
    # params / searchParams の非同期化
    npx @next/codemod@canary next-async-request-api .

    変換結果は正確でした。たとえば記事ページはこうなります。

    自動変換された箇所
    - type Props = { params: { slug: string } };
    + type Props = { params: Promise<{ slug: string }> };
    
    - export default async function BlogPostPage({ params }: Props) {
    + export default async function BlogPostPage(props: Props) {
    +   const params = await props.params;

    49ファイルを走査して2ファイルを変更、エラー0。手で直していたら見落としが出る種類の作業です。

    変換されなかった場所も、確認する

    「0 errors」は「全部やった」ではありません。ツールが認識できなかった書き方は、静かに素通りします。変更されたファイル数と、自分が想定していた箇所の数が合っているかは、自分で確かめる必要があります。今回は2ファイルで一致していました。

    入れる必要のない設定を、消した

    ここが今回いちばん判断が要った箇所です。

    コードモッドは、7つのファイルにこれを追加していました。

    追加されていたもの
    // TODO: Cache Components adoption. Refactor this route so this
    // opt-out can be removed.
    export const instant = false;

    調べると、これは16.3の新機能に対するオプトアウトで、別の設定を有効にしているときにだけ意味を持ちます。このサイトはその設定を使っていないので、7ファイルすべてで何もしていません。

    将来のために先回りして入れてくれたものです。しかし残すとどうなるかを考えました。

    残す場合 消す場合
    将来その機能を有効にしたとき、警告が出ない そのとき改めて入れる必要がある
    「あとで対応する」というTODOが7か所に残る コードに無効な設定が残らない

    消しました。誰も対応しないTODOは、そのうち景色になります。読むたびに「これは何だっけ」と確認するコストが7か所ぶん発生し、いずれ誰も読まなくなります。必要になったときに、必要な場所へ入れるほうが健全だと判断しました。

    自動生成されたものを、そのまま受け取らない

    コードモッドが入れたものは正しい提案です。しかし「このプロジェクトにとって必要か」までは判断していません。ツールは一般的に安全な側へ倒します。プロジェクトの事情を知っているのは書き手のほうです。

    本番に入れる前に、本番と同じもので確かめる

    今回はmainに直接入れず、ブランチを切りました。

    進め方
    git checkout -b feat/next-16
    (コードモッド実行・手直し)
    npm run build      ← 手元で通ることを確認
    git push -u origin feat/next-16

    ブランチをpushすると、デプロイ先がプレビュー用のURLを作ってくれます。本番と同じビルド手順で、本番と同じ環境に載った状態を、公開せずに触れます。

    そこで確認したのは6点です。

    確認項目
    1. 記事一覧が20本、サムネイルが出る
    2. 記事ページの本文・目次・読了バー
    3. sitemap.xml が25件(数を数える)
    4. robots.txt が4行
    5. 構造化データの日時に Z が付いている
    6. ビルド時のエラー・警告
    3番は、数えないと分からない

    sitemapは、失敗しても固定ページだけを返して200で応答する作りにしてあります。壊れていても画面は正常に見えます。「表示された」ではなく「25件あった」まで確認しないと、記事がsitemapから消えていても気づけません。

    全部通ったのを見てから、mainへマージしました。プレビューで通ったものが本番でも通る——確認の順番として、これが一番安く済みます。

    結果

    脆弱性の件数
    next     18件 → 0
    postcss   4件 → 0
    nanoid    1件 → 0
    glob      3件 → 0
    lodash    2件 → 2   (開発用のみ)
    
    合計     28件 → 2件

    glob が消えたのは想定外でした。ESLintも一緒に上がり、その依存として新しい glob が入ったためです。本番で動くもののうち、既知の脆弱性は無くなりました。

    残った2件は開発用の依存で、配信される成果物には入りません。

    今回の学び

    まず、「上げる」の前に、上げ先を数えること。14系の最新はすでに手元にあり、実行しても何も起きないコマンドを打ちかけていました。サポート期限を1つ調べるだけで防げます。

    次に、変更の小ささより、その変更が持つ期間で選ぶこと。15は変更が小さいですが2か月で切れます。安全に見える選択肢が、実は先送りなだけということがあります。

    そして、自動化ツールの出力も、読んでから受け取ること。コードモッドは正確でしたが、このプロジェクトに不要なものも入れていました。ツールは一般的に安全な側へ倒し、個別の事情までは知りません。

    最後に、本番と同じもので確かめてから本番に入れること。プレビュー環境を1回挟むだけで、「マージしてから気づく」が「マージ前に気づく」に変わります。確認の順番を変えただけで、リスクの大きさが変わります。

  • 依存の中の依存

    依存の中の依存

    pushしたら、GitHubからこう返ってきました。

    push時のメッセージ
    remote: GitHub found 29 vulnerabilities on kisaku39/kisaku-portfolio's
    remote: default branch (13 high, 14 moderate, 2 low).

    29件。数字だけ見ると、手のつけようがないように感じます。ところが中身を分類すると、実質3つの問題でした。

    この記事で扱うこと

    警告の件数をそのまま受け取らずに読み替える方法、自動で出てくる更新PRの中で「そのまま入れてよいもの」の見分け方、そして「直接の依存を上げたのに警告が消えない」という現象の正体。

    件数は、そのままの意味ではない

    一覧を開いて、パッケージ別に数え直しました。

    パッケージ 件数 用途
    next 19 本番で動く
    postcss 5 ビルド時に動く
    lodash 2 開発用のみ
    glob 1 開発用のみ
    nanoid 1 本番で動く

    29という数は、1つのパッケージに対する報告が積み上がった結果でした。nextの19件は、SSRF、Server ComponentsのDoS、画像最適化APIのDoS、キャッシュ混同——別々の問題ですが、直すのは「nextを上げる」の1手です。

    件数ではなく、手数を数える

    29件を29個の作業だと思うと、着手する気力が湧きません。実際に必要な操作は5つ以下でした。警告システムは問題の個数を報告しますが、こちらが知りたいのは作業の個数です。ここを読み替えないと、量に圧倒されて何もしないことになります。

    開発用かどうかで、優先度が変わる

    一覧には Development という表示が付くものがあります。lodash と glob がそれでした。

    これらはビルドや開発中にしか動かず、配信される成果物には入りません。つまり、サイトを見に来た人がこの脆弱性に触れることはありません。悪用するには、まず開発環境に入り込む必要があります。

    危険がゼロという意味ではありませんが、本番で動くものと同じ緊急度で扱う必要はありません。3件がここに該当したので、後回しにしました。

    自動で出てくるPRは、同じ安全さではない

    Dependabotは修正用のPRを自動で作ってくれます。5本ありました。

    開いていたPR
    #7   next               14.2.35 → 16.3.1    メジャー2つ跨ぎ
    #5   eslint-config-next 14.2.35 → 16.3.1    メジャー2つ跨ぎ
    #6   tailwindcss        3.4.4   → 4.3.3     メジャー跨ぎ
    #8   graphql-request    6.1.0   → 7.4.0     メジャー跨ぎ
    #10  postcss            8.5.23  → 8.5.26    パッチのみ

    ボタンひとつでマージできます。しかし、同じ「更新」でも中身の危険度がまったく違います。

    バージョンの変化 意味 扱い
    8.5.23 → 8.5.26 不具合修正のみ。使い方は変わらない そのまま入れてよい
    6.1.0 → 7.4.0 使い方が変わっている可能性がある 動作確認が要る
    14.2.35 → 16.3.1 2世代ぶんの変更が入る 移行作業として扱う
    GitHub上でマージすると、確認の機会が無い

    手元で更新する場合は npm run build で通ることを確かめてからpushできます。PRをブラウザでマージすると、その工程が丸ごと飛びます。通るかどうかは、本番のデプロイが動き出してから分かります。パッチ更新なら許容できますが、メジャー更新でこれをやるのは賭けです。

    そこで、#10だけをマージしました。チェックも2つとも通っていたので、これは安全な1本です。

    1件しか減らなかった

    マージ後、件数を数え直しました。

    結果
    29件 → 28件
    
    postcss  5件 → 4件

    postcssを上げたのに、postcssの警告が4件残っています。バージョンは 8.5.26 になっているはずです。

    残っている警告を1つ開くと、こう書いてありました。

    アラートの詳細
    影響を受けるバージョン   8.5.11 以下
    修正されたバージョン     8.5.12

    8.5.26 は 8.5.12 より新しいので、条件に当てはまりません。それなのに警告が残っている。

    依存の中の依存

    ロックファイルを直接見て、答えが出ました。

    package-lock.json 内の postcss
    8.5.23 (dev)  node_modules/postcss                ← 8.5.26 に更新された
    8.4.31        node_modules/next/node_modules/postcss ← 手つかず

    postcssが2つ入っていました。

    1つは自分で指定したもの。もう1つは next が自分の内側に抱えているものです。npmは、あるパッケージが要求するバージョンと自分の指定が食い違うとき、そのパッケージの中に別のコピーを置きます。同じ名前のものが、違うバージョンで同居できる仕組みです。

    なぜそういう作りになっているのか

    もし1つしか置けないなら、「AはpostCSS 8.4を要求、BはpostCSS 8.5を要求」という状況で、どちらかが壊れます。入れ子にすることで、互いの要求を同時に満たしています。便利な仕組みですが、その代わり「上げたつもりが上がっていない」が起こります。

    自分で指定した依存は、自分で上げられます。他のパッケージが抱えている依存は、そのパッケージ自身が上げてくれるのを待つしかありません。

    同じことが nanoid にも起きていました。これも next 経由で入っています。

    読み替えた結果

    ここまでを踏まえて、28件を整理し直しました。

    分類 件数 必要な操作
    next 本体 18 next を上げる(1手)
    next が抱える postcss 4
    next が抱える nanoid 1
    開発用(lodash / glob) 5 本番に影響なし。後回し

    28件のうち23件が、1つの操作に集約されました。最初に見た「29件」という数字とは、受ける印象がまったく違います。

    それでも、一気には上げない

    では next を16に上げればいい——とはなりませんでした。

    14から16は、2世代ぶんの変更が入ります。このサイトはApp Routerを使い、ISRで事前生成し、つい先ほどsitemapと構造化データを入れたばかりです。それらが動かなくなる可能性を、確認せずに本番へ出すことはできません。

    段階を分けました。

    今日やること
    npm install next@14 eslint-config-next@14   # 14系の最新まで
    npm run build                               # 通るか確認
    npm run start                               # 本番と同じ条件で表示確認

    まずメジャーを跨がずに取れる分だけ取ります。これで消えない分が、16に上げる判断の材料になります。「全部消したい」から一気に上げると、落ちたときに何が原因か分かりません。

    確認は dev ではなく start で

    開発サーバー(npm run dev)と本番(npm run build → npm run start)では動きが違います。sitemapや構造化データのようにビルド時に生成されるものは、devで見ても本番と同じとは限りません。更新の影響を確かめる目的なら、本番と同じ手順で見る必要があります。

    今回の学び

    まず、警告の件数と、作業の件数は違うこと。29件のうち23件が1つの操作に集約されました。件数のまま受け取ると、量に圧倒されて着手できなくなります。パッケージ別に数え直すだけで、見える景色が変わります。

    次に、「更新」の一言に、危険度の違う操作が混ざっていること。パッチ更新とメジャー更新は、ボタンの見た目が同じでもやっていることが違います。自動で提案されたからといって、同じ扱いをしてよいわけではありません。

    そして、直接の依存を上げても、内側の依存は動かないこと。同じパッケージが違うバージョンで同居できる仕組みがあるため、「上げたのに消えない」が起こります。ロックファイルを見れば10秒で分かる話ですが、知らないと延々と悩みます。

    最後に、本番に届くものと、届かないものを分けること。開発用の依存は、同じ「高」でも読者に届きません。全部を等しく緊急扱いすると、本当に急ぐものに手が回らなくなります。