フォームを作っていると、「ユーザーには入力させたくないけれど、サーバーには一緒に送りたい値」が出てきます。編集中の投稿の ID や、どの処理を実行するかの種別、セキュリティ対策のトークンなどです。こうした値を運ぶために用意されているのが <input type="hidden"> です。この記事では、画面に表示されないのに送信される仕組み、hidden 属性や display: none との役割の違い、実際の使いどころ、そして「hidden の値は信用してはいけない」というセキュリティ上の大前提までを解説します。
目次
input type=”hidden” とは何か
<input> の type 属性に hidden を指定すると、画面にはまったく描画されないのに、フォームを送信すると name と value のペアがサーバーへ送られる入力欄になります。テキスト欄やチェックボックスのように「ユーザーが値を決める」ための部品ではなく、開発者があらかじめ決めた値をフォームに同梱するための入れ物、と考えると分かりやすいです。
描画されないので、幅も高さも持たず、レイアウトにも一切影響しません。ユーザーがフォーカスを当てることもできないため、<label> を関連付ける必要もありませんし、スクリーンリーダーにも読み上げられません。書き方はとてもシンプルで、必要なのは name と value の2つだけです。
<input type="hidden" name="post_id" value="123">
この1行をフォームの中に置いておくと、送信時に post_id=123 という値が一緒に送られます。name はサーバー側で値を取り出すときのキー、value は送りたい値そのものです。value を書き忘れると空文字列が送られるだけで、エラーにはならないので注意してください。
基本の書き方とサーバー側での受け取り
実際のフォームに組み込んだ例を見てみましょう。記事タイトルを編集して更新するフォームに、どの記事を編集しているかを示す ID を忍ばせています。
<form action="/update.php" method="post">
<!-- 画面には出ないが送信される値 -->
<input type="hidden" name="post_id" value="123">
<label for="title">記事タイトル</label>
<input type="text" id="title" name="title" value="hidden の使い方">
<button type="submit">更新する</button>
</form>
ユーザーの画面にはラベル付きのテキスト欄と更新ボタンしか見えませんが、送信されるデータには post_id と title の2つが含まれます。method="post" なので、受け取る側の PHP では $_POST からキーを指定して取り出します。
<?php
// hidden で送られた値も、他の入力欄と同じように $_POST に入る
$post_id = filter_input( INPUT_POST, 'post_id', FILTER_VALIDATE_INT );
$title = $_POST['title'] ?? '';
// 整数として解釈できなければ不正な値として扱う
if ( false === $post_id || null === $post_id ) {
http_response_code( 400 );
exit( '不正なリクエストです' );
}
// ここで初めて「この ID の記事を更新してよいか」を確認する
ポイントは、hidden で送られた値も、テキスト欄に入力された値とまったく同じ扱いになるということです。PHP から見れば $_POST['post_id'] も $_POST['title'] も区別がつきません。「hidden だから安全な値が入っているはず」という前提は成り立たないので、受け取り側では必ず検証します。この点については後半のセキュリティの章で詳しく説明します。
なお method="get" のフォームなら、hidden の値は URL のクエリ文字列に ?post_id=123&title=... の形で載ります。GET の場合は値がアドレスバーに丸見えになるので、そもそも隠す意味はほとんどありません。
hidden 属性・display: none との違い
「要素を隠す」方法には、type="hidden" のほかに hidden 属性と CSS の display: none があります。名前が似ているので混同しやすいのですが、この3つは目的がまったく違います。
| 書き方 | 正体 | フォーム送信 |
|---|---|---|
<input type="hidden"> | そもそも表示を持たない種類の入力欄 | 送信される |
<input type="text" hidden> | 表示できる入力欄を、HTML の属性で非表示にしたもの | 送信される |
<input type="text" style="display:none"> | 表示できる入力欄を、CSS で非表示にしたもの | 送信される |
送信されるかどうかという点では3つとも同じです。フォームの送信データは「見えているか」ではなく「フォームに属していて、name があり、無効化されていないか」で決まるからです。ここを誤解して「CSS で隠したから値は送られない」と思い込むと、意図しないデータを送ってしまいます。
役割の違い ── 隠された入力欄か、表示を持たない入力欄か
hidden 属性や display: none は、本来ユーザーが操作できる入力欄を、一時的に見せていない状態です。「配送先が自宅なら住所欄を隠す」のように、条件によって表示を切り替える用途で使います。あとで表示を戻せば、ユーザーがそこに入力できます。
対して type="hidden" は、最初からユーザーが操作することを想定していない入力欄です。表示を戻すという概念がなく、CSS で display: block を指定しても画面には現れません。「隠しているのではなく、表示を持たない種類の入力欄である」というのが正確な理解です。ユーザーに見せたり隠したりしたい入力欄には hidden 属性を、機械的に値を運びたいだけなら type="hidden" を選びます。
バリデーションの扱いも違う
もうひとつ大きな違いが、ブラウザの入力チェック(制約検証)の対象になるかどうかです。type="hidden" の入力欄は制約検証の対象から外されますが、hidden 属性や display: none で隠しただけの入力欄は対象のままです。そのため、隠した入力欄に required が付いたままだと「必須なのに入力されていない」と判定され、しかもエラーメッセージを表示すべき欄が画面にないため、送信できないのに理由も分からない、という厄介な状態になります。隠すときは required を外す、あるいは disabled にする、といった後始末が必要です。
実践的な使いどころ
ここからは、実際の開発で type="hidden" が登場する典型的な場面を4つ紹介します。いずれも「ユーザーの入力ではないが、リクエストに含めたい文脈情報」を運ぶ、という共通点があります。
編集フォームで対象のレコード ID を送る
もっとも定番の使い方です。一覧画面から「編集」を選んでフォームを開いたとき、そのフォームがどのレコードのものかをサーバーに伝える必要があります。URL に含める方法もありますが、POST で送るフォームなら hidden に入れておくのが自然です。
<form action="/update.php" method="post">
<!-- 出力時は必ずエスケープする -->
<input type="hidden" name="post_id" value="<?php echo htmlspecialchars( (string) $post->id, ENT_QUOTES, 'UTF-8' ); ?>">
<label for="title">タイトル</label>
<input type="text" id="title" name="title" value="<?php echo htmlspecialchars( $post->title, ENT_QUOTES, 'UTF-8' ); ?>">
<button type="submit">更新する</button>
</form>
value に変数を埋め込むときは、必ず htmlspecialchars() などでエスケープします。画面に表示されないからといって油断すると、値に " が含まれた瞬間に属性が閉じられ、HTML が壊れたり任意のタグを差し込まれたりします。表示されない要素でも、HTML として出力している以上は同じ注意が必要です。
同じ送信先で処理の種類を分ける
1つのページに「下書き保存」「公開」「削除」といった複数のフォームが並ぶことがあります。送信先の PHP が同じなら、どの操作なのかを hidden で伝えると、サーバー側で処理を振り分けられます。
<form action="/handle.php" method="post">
<input type="hidden" name="post_id" value="123">
<input type="hidden" name="mode" value="publish">
<button type="submit">公開する</button>
</form>
<form action="/handle.php" method="post">
<input type="hidden" name="post_id" value="123">
<input type="hidden" name="mode" value="delete">
<button type="submit">削除する</button>
</form>
サーバー側では $_POST['mode'] を見て分岐します。ただし、hidden の値はあくまで「クライアントの自己申告」です。mode が delete だからといって無条件に削除してよいわけではなく、そのユーザーに削除権限があるかは別途サーバーで確認します。
なお、フォームを分けずに1つにまとめたい場合は、送信ボタン自体に name と value を付ける方法もあります。<button type="submit" name="mode" value="publish"> と書くと、押されたボタンの値だけが送信されるため、押されたボタンを判別できます。フォームが1つで操作だけ分けたいときはこちらが簡潔です。
CSRF 対策のトークンを埋め込む
CSRF(クロスサイトリクエストフォージェリ)は、攻撃者が用意した別サイトのページから、ログイン中のユーザーに意図しないリクエストを送らせる攻撃です。対策として、サーバーがフォームを出力するときにワンタイムの文字列(トークン)を hidden に埋め込んでおき、送信されてきたトークンがサーバー側で保持しているものと一致するかを検証します。
<?php
session_start();
// トークンがなければ生成してセッションに保存しておく
if ( empty( $_SESSION['csrf_token'] ) ) {
$_SESSION['csrf_token'] = bin2hex( random_bytes( 32 ) );
}
?>
<form action="/handle.php" method="post">
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars( $_SESSION['csrf_token'], ENT_QUOTES, 'UTF-8' ); ?>">
<label for="title">タイトル</label>
<input type="text" id="title" name="title">
<button type="submit">送信</button>
</form>
<?php
session_start();
$sent = $_POST['csrf_token'] ?? '';
$saved = $_SESSION['csrf_token'] ?? '';
// hash_equals() でタイミング攻撃を避けつつ比較する
if ( '' === $saved || ! hash_equals( $saved, $sent ) ) {
http_response_code( 403 );
exit( 'リクエストが無効です' );
}
// ここから本来の処理
攻撃者のサイトからは、このトークンの値を知ることができません。だからこそ「正しいトークンが付いている=自サイトのフォームから送られてきた」と判断できる、というのがこの仕組みの要点です。WordPress では同じ考え方が wp_nonce_field() として用意されていて、呼び出すだけで hidden の入力欄が出力されます。
送信後のリダイレクト先を持たせる
ログインフォームのように「処理が終わったら元のページに戻したい」場面では、戻り先の URL を hidden に入れておく方法がよく使われます。
<form action="/login.php" method="post">
<input type="hidden" name="redirect_to" value="/mypage/orders">
<label for="email">メールアドレス</label>
<input type="email" id="email" name="email">
<button type="submit">ログイン</button>
</form>
ただしこの使い方には落とし穴があります。送られてきた URL をそのまま Location ヘッダーに渡すと、値を書き換えられて外部サイトへ飛ばされる「オープンリダイレクト」になります。受け取った値は、自サイト内の相対パスかどうかを検証してから使ってください。許可するパスをあらかじめ一覧で持っておき、その中にあるものだけを採用するのが確実です。
JavaScript で hidden の値を読み書きする
hidden の値は JavaScript からも自由に読み書きできます。フォーム要素の elements プロパティに name を指定すると、その入力欄を取り出せます。
const form = document.querySelector('#edit-form');
// name を指定して hidden の入力欄を取り出す
const postId = form.elements.post_id;
console.log(postId.value); // "123"
// 値を書き換える
form.elements.post_id.value = '456';
// FormData にも hidden の値は含まれる
const formData = new FormData(form);
console.log(formData.get('post_id')); // "456"
new FormData(form) で作ったオブジェクトには、hidden も含めてフォーム内の送信対象がすべて入ります。fetch() で非同期に送信するときも、hidden に入れた ID やトークンをそのまま持っていけるということです。
注意したいのは、JavaScript で value を書き換えても input や change イベントは発生しない点です。これは hidden に限らず JavaScript による値の代入全般に言えることですが、hidden は仕様上これらのイベントが適用されない種類の入力欄なので、「値の変化を change で監視する」という発想は使えません。値を変えたタイミングで何かしたいなら、変更する側のコードで明示的に処理を呼びます。
下のデモでは、hidden を2つ含むフォームの送信内容を FormData で取り出して表示しています。画面には title の欄しか見えていませんが、ボタンを押すと post_id と action も一緒に送られようとしていることが確認できます。
<form id="edit-form">
<!-- 画面には表示されないが送信される -->
<input type="hidden" name="post_id" value="123">
<input type="hidden" name="action" value="update">
<label for="title">記事タイトル</label>
<input type="text" id="title" name="title" value="hidden の使い方">
<button type="submit">送信される値を見る</button>
</form>
<pre id="output">ボタンを押すと、送信される name と value が表示されます。</pre>
body {
font-family: sans-serif;
padding: 20px;
margin: 0;
}
#edit-form {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 360px;
}
label {
font-size: 14px;
color: #555;
}
#title {
padding: 8px 12px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 6px;
}
button {
align-self: flex-start;
padding: 8px 16px;
font-size: 15px;
border: none;
border-radius: 6px;
background: #007bff;
color: #fff;
cursor: pointer;
}
#output {
margin-top: 16px;
padding: 12px;
background: #f5f5f5;
border-radius: 6px;
font-size: 14px;
line-height: 1.7;
white-space: pre-wrap;
}
const form = document.getElementById('edit-form');
const output = document.getElementById('output');
form.addEventListener('submit', (event) => {
// デモなので実際の送信は止める
event.preventDefault();
// FormData には hidden の値も含まれる
const formData = new FormData(form);
const lines = [];
for (const [name, value] of formData.entries()) {
lines.push(name + ' = ' + value);
}
output.textContent = lines.join('\n');
});
hidden の値は信用してはいけない
ここが type="hidden" を扱ううえで最も重要な話です。hidden は「ユーザーから隠す」ための機能ではありません。単に画面に描画されないだけで、値そのものは HTML に平文で書かれており、誰でも見ることも書き換えることもできます。
開発者ツールで簡単に書き換えられる
ブラウザでページのソースを表示すれば <input type="hidden" name="price" value="1200"> という記述はそのまま読めますし、開発者ツールの要素パネルから value を 1 に書き換えて送信することもできます。コンソールで form.elements.price.value = 1 と実行するだけでも同じです。特別な知識や道具は必要ありません。
そもそもフォームを経由しなくても、任意のリクエストを組み立てて送ることはできます。つまり、サーバーが受け取る値は「クライアントが送ってきた文字列」でしかなく、それ以上の保証はどこにもありません。
改ざんされると困る値を入れない
したがって、商品の価格、割引率、ユーザーの権限やロール、ログイン中のユーザー ID といった「書き換えられては困る値」を hidden に入れてはいけません。価格を hidden で送って、その値で決済してしまう作りにすると、1円で買われてしまいます。<input type="hidden" name="role" value="user"> のような実装なら、admin に書き換えるだけで管理者になれてしまいます。
こうした値は、そもそもクライアントに往復させず、サーバー側で持つのが正しい設計です。価格は送られてきた商品 ID からサーバーがデータベースを引いて決め、ログイン中のユーザーはセッションから取得します。
hidden に入れてよい値と、必ず行う検証
では何を入れてよいのかというと、「書き換えられても、サーバー側の検証で弾けるもの」です。たとえば投稿 ID は hidden に入れて構いませんが、受け取ったあとに「その ID の投稿は存在するか」「ログイン中のユーザーはそれを編集する権限があるか」をサーバーで必ず確認します。この確認があれば、他人の投稿 ID に書き換えられても拒否できます。
<?php
$post_id = filter_input( INPUT_POST, 'post_id', FILTER_VALIDATE_INT );
// 1. 値の形式を検証する
if ( false === $post_id || null === $post_id ) {
http_response_code( 400 );
exit( '不正なリクエストです' );
}
// 2. その投稿が存在するかを確認する
$post = find_post( $post_id );
if ( ! $post ) {
http_response_code( 404 );
exit( '投稿が見つかりません' );
}
// 3. ログイン中のユーザーに編集権限があるかを確認する
// ユーザー ID は hidden ではなくセッションから取る
if ( $post->author_id !== ( $_SESSION['user_id'] ?? null ) ) {
http_response_code( 403 );
exit( '権限がありません' );
}
// ここまで通って初めて更新処理を実行する
前の章で紹介した CSRF トークンも、「値を隠しているから安全」なのではありません。トークンは推測できないランダムな文字列で、かつサーバー側が保持している値と照合するからこそ意味があります。hidden はトークンを運ぶ手段にすぎず、安全性を担保しているのはサーバー側の照合処理だという点を押さえておきましょう。
required・disabled はどう扱われるか
required や pattern は効かない
type="hidden" の入力欄は、ブラウザの制約検証(入力チェック)の対象から外されます。そのため required を書いても無視され、値が空のままでもフォームは送信できます。同じく pattern、maxlength、minlength、readonly、placeholder といった属性も type="hidden" には適用されません。
<!-- required は効かない。空のまま送信できてしまう -->
<input type="hidden" name="post_id" value="" required>
これは欠陥ではなく、ユーザーが操作できない欄でエラーが出ても、修正のしようがないからです。仮に検証対象だったとしたら、画面のどこにもない欄のせいで送信できず、メッセージも表示できない、という状況になってしまいます。hidden の値が正しいかどうかのチェックは、ブラウザに任せず JavaScript かサーバー側で行うのが前提だと考えてください。
disabled にすると送信されない
一方で disabled は hidden にも適用されます。無効化された入力欄はフォームの送信データに含まれないため、<input type="hidden" name="mode" value="publish" disabled> と書くと mode は一切送られません。条件によって値を送ったり送らなかったりしたい場合に、JavaScript から disabled を切り替えるという使い方ができます。
逆に、これは思わぬ不具合の原因にもなります。フォーム全体を <fieldset disabled> で囲んで入力を一時的に止めると、その中にある hidden も無効化され、ID やトークンが送られなくなります。送信中にフォームをまとめて無効化する処理を書くときは、hidden がその範囲に入っていないかを確認しておきましょう。
_charset_ という特別な name
type="hidden" には、name に _charset_ を指定したときだけの特別な挙動があります。この名前を付けると、その入力欄の値はブラウザが自動的に「フォームの送信に使われた文字エンコーディング」に設定します。自分で value を書く必要はありません。
<form action="/search.php" method="get">
<!-- 送信時に value が UTF-8 などに自動で埋められる -->
<input type="hidden" name="_charset_">
<input type="search" name="q">
<button type="submit">検索</button>
</form>
サーバー側では $_GET['_charset_'] を見れば、どのエンコーディングで送られてきたかが分かります。ページの文字コードが UTF-8 に統一されている現在の一般的な構成ではまず必要になりませんが、複数のエンコーディングが混在する古いシステムを扱うときに役立つ仕組みです。
値が届かないときに確認すること
name 属性が付いているか
フォームの送信データは name をキーにして組み立てられるため、name のない入力欄は値を持っていても送信されません。id だけを付けて name を書き忘れる、というのはよくあるミスです。画面に出ないぶん書き間違いに気づきにくいので、値が届かないときはまずここを疑ってください。
フォームの外に置かれていないか
見た目に影響しない要素なので、つい <form> の外に書いてしまうことがあります。当然ながら、フォームに属していない入力欄は送信されません。テーブルの中にフォームを組んだ場合など、HTML の構造上 <form> の中に置けないケースもありますが、そのときは form 属性にフォームの id を指定すれば、離れた場所にあっても同じフォームの一部として送信できます。
<form id="edit-form" action="/update.php" method="post">
<input type="text" name="title">
<button type="submit">更新する</button>
</form>
<!-- form 属性でフォームに紐づければ、外にあっても送信される -->
<input type="hidden" form="edit-form" name="post_id" value="123">
それでも届かない場合は、前の章で触れた disabled が付いていないか、JavaScript が送信直前に値を書き換えていないかも確認します。ブラウザの開発者ツールのネットワークタブで、実際に送られたリクエストの内容を見るのが最短の確認方法です。
まとめ
<input type="hidden"> は、画面に描画されないまま name と value をフォーム送信に含めるための入力欄です。hidden 属性や display: none は「表示できる入力欄を隠している」状態で、送信される点は同じですが、type="hidden" はそもそも表示を持たない別種の入力欄であり、制約検証の対象外になるという違いがあります。編集対象の ID、処理の種別、CSRF トークン、リダイレクト先といった文脈情報を運ぶのが典型的な使い方で、JavaScript からは form.elements.名前.value や FormData で読み書きできます。required は効かず、disabled にすると送信されない点も押さえておきましょう。そして何より大切なのは、hidden の値は開発者ツールで誰でも書き換えられるということです。価格や権限のような改ざんされて困る値は入れず、受け取った値はサーバー側で必ず検証してください。