Xserver のサーバーキャッシュで、お問い合わせフォームが送れなくなる理由と対処

Xserver のサーバーキャッシュに PHP のお問い合わせフォームが載ると、全員に同じ CSRF トークン(送信を確かめる「券」)が配られて送れなくなります。対処は Cache-Control: no-store, private を毎回返すこと。session_start の落とし穴と、本番で curl で確かめる方法まで。

お問い合わせのページの画面と「Cache-Control: no-store, private」の文字

Xserver のサーバーキャッシュでお問い合わせフォームが送れなくなる原因は、フォームのページがキャッシュに載り、開いた全員に同じ CSRF トークン(送信を確かめるための「券」)が配られることです。対処は、どの応答でも「Cache-Control: no-store, private」を返すことと、session_start() の前に session_cache_limiter('') を呼んで、その見出しを上書きさせないことです。

公式サイトを WordPress から静的なサイトへ移した時、お問い合わせのフォームは PHP で新しく作りました。ほかのページはただの HTML ですが、このページだけは、開いた人ごとに違う中身を返す必要があります。

ここで引っかかったのが、Xserver の「サーバーキャッシュ」です。私たちのサイトでは、サーバーパネルの設定で、すべてのページがサーバーキャッシュの対象になっていました。何も手を打たないと、お問い合わせのページがキャッシュに載り、ある時から誰も送信できなくなるおそれがありました。

この記事は、その仕組みと、私たちが選んだ対処、本番での確かめ方の記録です。

公式サイトのお問い合わせのページ。「ご相談は、こちらから。」の見出しと入力欄
お問い合わせのページ。表示も、エラーも、受付も、この1つの PHP が返す

フォームは、開いた人ごとに違う「券」(CSRF トークン)を配っている

私たちのフォームは、ページを開くたびに、その人だけの「券」(ランダムな文字列)を作ります。券はページの中に隠して入れ、同じものをサーバー側にも、その人の記録(セッション)として覚えておきます。送信された時に、ページから届いた券と、サーバーが覚えている券が一致した時だけ受け付けます。

お問い合わせのフォームは、ページを開くたびにその人だけの券(CSRF トークン)を作り、ページとサーバー側のセッションの両方に覚え、送信された時に一致した時だけ受け付ける
券は、勝手な送信や二重送信を防ぐためのもの

この券は、ほかのサイトから勝手に送信させる攻撃や、同じ内容を二度送ってしまう事故を防ぐためのものです。人には見えない入力欄(ハニーポット)や、開いてから3秒未満の送信を断る仕組みと組み合わせて、迷惑な送信を減らしています。

キャッシュに載ると、全員に同じ券が配られる

サーバーキャッシュは、一度作ったページの中身をしばらく取っておき、次に開いた人には PHP を動かさずに同じ中身を返す仕組みです。ただの HTML のページなら、表示が速くなるだけで困りません。

店のカウンターに置かれた、カード決済の端末の写真
キャッシュに載ると、全員に同じ券が配られる(イメージ写真)

ところが、お問い合わせのページがキャッシュに載ると、キャッシュに入った「誰か1人の券」が、そのあと開いた全員に配られます。その人たちのセッションには、その券がありません。送信しても券が一致せず、受け付けられません。

Aさんの1回目は Set-Cookie があるのでキャッシュに入らない。2回目は Set-Cookie が無いので、券の入ったページがキャッシュに入る。そのあと開いたBさんたち全員に同じ券が配られ、送信できなくなることを示した図
1回目の訪問では問題が見えないのが、この落とし穴のやっかいなところ

Xserver のマニュアルの決まり

Xserver のマニュアル(サーバーキャッシュ設定)には、キャッシュの対象にしない応答の条件が書かれています。2026年10月11日にマニュアルのページを読んで確かめたところ、次の応答はキャッシュされません。

  • 応答の見出し Cache-Control に、「no-cache」「no-store」「private」「must-validate」のどれかが含まれるもの(表記はマニュアルのまま)
  • 応答の見出しに Set-Cookie が含まれるもの
  • 応答の番号が 200 と 404 以外のもの(転送の 303 などは、もともとキャッシュされない)
www.xserver.ne.jpサーバーキャッシュ設定(Xserver のマニュアル)キャッシュの対象にならない応答の条件が書かれている

ここで、「セッションを始めると Set-Cookie が付くのだから、キャッシュされないのでは」と考えたくなります。けれども、それで守られるのは初めての訪問だけです。

はじめは、.htaccess で /contact/ だけをサーバーキャッシュの対象から外す行も用意していました。けれども、マニュアルにはっきり書いてある Cache-Control の条件で足りるので、その行は外し、PHP の見出しだけに頼ることにしました。設定の場所が1か所になるので、あとから読んだ人にも分かりやすくなります。

Xserver のマニュアルで、キャッシュされない応答の条件と、私たちの選択の図
キャッシュされない条件と、私たちの選択

session_start が Cache-Control を上書きする落とし穴

PHP で Cache-Control を返すだけなら、header() を1行書けば済みます。ところが、そのあとに session_start() を呼ぶと、PHP はセッションの設定(session.cache_limiter)に従って、自分で Cache-Control などの見出しを送ります。同じ名前の見出しは置き換わるので、先に書いた値が消えてしまいます。

白い紙の束を、横から写した写真
同じ紙を何枚も配ると、送れなくなる(イメージ写真)
header() で Cache-Control を返しても、そのあとの session_start() が PHP のセッションの設定に従って自分で Cache-Control を送り同じ名前の見出しを置き換える。session_cache_limiter に空文字を渡して送らせない
session_start の前に session_cache_limiter('') を呼ぶ

既定の設定なら、送られる値にも no-store が含まれます。ただ、それはサーバーの PHP の設定次第です。キャッシュしてよいという値の設定もあり、そうなっていればフォームはキャッシュに載ります。私たちは、確かめていないサーバーの設定に任せたくありませんでした。

PHPcontact/index.php
<?php
// どの応答でも、必ず最初にこの見出しを返す(Set-Cookie には頼らない)
header('Cache-Control: no-store, private');
// session_start() が自分の Cache-Control を送って、上の見出しを上書きしないようにする
session_cache_limiter('');

session_name('contact');
session_set_cookie_params([
    'path'     => '/contact/',
    'secure'   => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);
session_start();

// 開くたびに、その人だけの券を作る
$_SESSION['contact_token'] = bin2hex(random_bytes(16));

本番で、券が毎回違うことを確かめる

仕組みを作ったら、本番のサーバーで実際に確かめます。Mac のターミナルから curl で開くだけなので、サーバーに入る必要はありません。

  1. 1回目:Cache-Control と Set-Cookie が両方あること

    Macで実行
    curl -s -D - -o /dev/null -c /tmp/c.jar -b /tmp/c.jar https://あなたのドメイン/contact/ | grep -iE '^(cache-control|set-cookie)'
    cache-control: no-store, private
    set-cookie: contact=(値); path=/contact/; secure; HttpOnly; SameSite=Lax
  2. 2回目:Set-Cookie が無くても、Cache-Control があること

    クッキーを持ったまま、もう一度開きます。ここで Cache-Control が出なければ、キャッシュに載るおそれがあります。

    Macで実行
    curl -s -D - -o /dev/null -c /tmp/c.jar -b /tmp/c.jar https://あなたのドメイン/contact/ | grep -iE '^(cache-control|set-cookie)'
    cache-control: no-store, private
  3. 開くたびに、券が違うこと

    クッキーを持たずに2回開き、ページの中の券を見比べます。同じ値が出たら、キャッシュに載っています。

    Macで実行
    for i in 1 2; do curl -s https://あなたのドメイン/contact/ | grep -o 'name="token" value="[^"]*"' | cut -c1-30; done
    name="token" value="0397edfe02
    name="token" value="327ebb0e14
    rm /tmp/c.jar

私たちのサイトでも、この3つを本番で確かめました(券の値は、頭の一部だけを載せています)。

ターミナルの再現。1回目も2回目も Cache-Control: no-store, private が返り、クッキー無しで開くたびに券の値が違う
本番での確かめ(券は頭の一部だけ)

まとめ

Xserver のサーバーキャッシュで、お問い合わせフォームが送れなくなる理由と対処:要点のまとめ。サーバーキャッシュに載ったフォームは、全員に同じ券を配り、送信できなくなる
この記事の要点
  • サーバーキャッシュに載ったフォームは、全員に同じ券を配り、送信できなくなる
  • Xserver のマニュアルでは、Cache-Control に no-store や private がある応答と、Set-Cookie がある応答はキャッシュされない
  • Set-Cookie は初めての訪問でしか返らないので、どの応答でも毎回 Cache-Control: no-store, private を返す
  • session_start() が見出しを上書きしないよう、先に session_cache_limiter('') を呼ぶ
  • 本番では curl で、見出しと、券が毎回違うことを確かめ、最後に1通送る

このフォームを作った、サイト全体の移行の記録はこちらです。

新しいトップページの画面と「WordPress → 静的サイト」の文字ホームページ制作WordPress から静的サイトへ、社名で検索1位を守ったまま移行した全手順会社のサイトを WordPress から静的サイト(HTML のファイルを置くだけのサイト)へ移した全手順です。社名で検索1位を守るための 301 リダイレクトの洗い出し、バックアップ、誰にも見せないリハーサル、表示を止めない切り替えの順番、すぐ戻せる作りまでを、実際の手順書から書きます。

この記事は、AI(Claude)の支援を受けて作成し、筆者が内容を確認しています。

  • Xserver
  • サーバーキャッシュ
  • PHP
  • お問い合わせフォーム
  • Cache-Control
  • session

関連する記事

この記事に関連するサービス・アプリ

ブログの一覧へ