HTMLの<input type="hidden">を使うと、画面に入力欄を表示せず、ほかのform dataと一緒に値を送信できます。
<form action="update.php" method="post">
<input type="hidden" name="post_id" value="42">
<label>
タイトル
<input type="text" name="title" required>
</label>
<button type="submit">更新する</button>
</form>
PHPではname="post_id"の値を$_POST['post_id']またはfilter_input()で取得できます。
<?php
$postId = filter_input(
INPUT_POST,
'post_id',
FILTER_VALIDATE_INT,
['options' => ['min_range' => 1]],
);
if ($postId === false || $postId === null) {
http_response_code(400);
exit('post_idが正しくありません');
}
ただし、hiddenは「画面に見えない」だけです。利用者はDevToolsやHTTP requestを使って値を変更できます。価格、税率、権限、所有者IDなど重要な値をhiddenから受け取って、そのまま信用してはいけません。
hiddenフィールドとは
input type="hidden"は、form送信時に値を含めたいものの、利用者が画面上で直接編集するcontrolとして表示しない場合に使います。
代表例は次のとおりです。
- 編集対象のrecord ID
- 複数step formの識別子
- 選択したtabや遷移元などのUI状態
- serverが発行したCSRF token
- 同じnameを持つ配列値
nameがないinputはform dataへ含まれません。送信するにはnameとvalueを設定します。
hidden inputの特徴
- rendered pageでは入力controlとして表示されない
- keyboardやJavaScriptの
focus()でfocusできない input・changeeventの対象にならない- constraint validationへ参加しない
- 値はsource・DevTools・requestから確認・変更できる
requiredやminを付けてhidden値を保護することはできません。server側のvalidationが必要です。
PHPでhidden値をPOSTして取得する
2つのfileを使う最小例です。
index.php
receive.php
index.php:formを作る
<form action="receive.php" method="post">
<input type="hidden" name="article_id" value="42">
<label>
コメント
<textarea name="comment" required></textarea>
</label>
<button type="submit">送信する</button>
</form>
HTML内に固定値を直接書く代わりにPHP変数を出力する場合は、attribute contextに合わせてescapeします。
<input
type="hidden"
name="article_id"
value="<?= htmlspecialchars(
(string) $articleId,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
) ?>"
>
整数だと分かっていても、HTMLへ出力する境界でescapeする方針を統一すると、後で値の種類が変わったときの事故を防ぎやすくなります。
receive.php:POST値を検証する
<?php
$articleId = filter_input(
INPUT_POST,
'article_id',
FILTER_VALIDATE_INT,
['options' => ['min_range' => 1]],
);
$comment = filter_input(INPUT_POST, 'comment', FILTER_UNSAFE_RAW);
if ($articleId === false || $articleId === null) {
http_response_code(400);
exit('article_idが正しくありません');
}
if (!is_string($comment) || trim($comment) === '') {
http_response_code(400);
exit('コメントを入力してください');
}
$comment = trim($comment);
FILTER_DEFAULTはFILTER_UNSAFE_RAWのaliasであり、既定で安全なvalidationが行われるわけではありません。IDは整数、statusはallowlist、emailはemailというように、用途に合うruleを明示します。
hidden値は文字列として届く
HTTP formから届くscalar valueは、基本的にstringとして扱います。HTMLでvalue="42"と書いても、PHPへ整数42として保証されるわけではありません。
var_dump($_POST['article_id'] ?? null);
// string(2) "42"
整数として使う前にFILTER_VALIDATE_INTやstrictなapplication validationを通します。単に(int)へcastすると、"abc"も0になり、invalid inputを見分けにくくなります。
hidden値を信用してはいけない理由
利用者は次のような方法でhidden値を変更できます。
- DevToolsのElements panelでvalueを書き換える
- JavaScriptで
input.valueを変更する - network requestを再送・編集する
- 独自のHTTP clientから同じendpointへ送る
OWASPは、hidden control、disabled control、JavaScriptが設定したfieldなど、UIで編集できないように見える値もすべて外部入力として扱い、server側でbusiness ruleを検証するよう求めています。
送ってはいけない値
次の値をhiddenから受け取り、そのまま確定処理へ使わないでください。
- 商品価格、割引率、税率、送料
is_admin、role、permission- 利用者本人を表すuser ID
- 注文済み・支払済みなどのstatus
- 任意のredirect URLやfile path
clientからは対象を特定するIDや選択内容だけを受け取り、serverでdatabase・session・認証情報から正しい値を再取得します。
価格・税率はserver側から取得する
旧記事ではquantity、price、taxをすべてformから受け取り、さらに税額を減算していました。価格と税率をclientへ計算のsource of truthとして渡す設計は、改ざんに弱くなります。
formではproduct IDとquantityだけ送ります。
<form action="calculate.php" method="post">
<input type="hidden" name="product_id" value="42">
<label>
購入数量
<input type="number" name="quantity" min="1" max="20" required>
</label>
<button type="submit">金額を確認する</button>
</form>
serverで両方を検証し、product recordから価格と税率を取得します。
<?php
$productId = filter_input(
INPUT_POST,
'product_id',
FILTER_VALIDATE_INT,
['options' => ['min_range' => 1]],
);
$quantity = filter_input(
INPUT_POST,
'quantity',
FILTER_VALIDATE_INT,
['options' => [
'min_range' => 1,
'max_range' => 20,
]],
);
if ($productId === false || $productId === null ||
$quantity === false || $quantity === null) {
http_response_code(400);
exit('入力値が正しくありません');
}
// 実際はprepared statementなどでdatabaseから取得する
$product = findProductById($productId);
if ($product === null || !$product['is_available']) {
http_response_code(404);
exit('商品が見つかりません');
}
$subtotal = $product['unit_price'] * $quantity;
$tax = intdiv($subtotal * $product['tax_rate_percent'], 100);
$total = $subtotal + $tax;
金額の丸めruleは通貨・契約・会計要件に合わせ、整数の最小通貨単位などで統一します。hidden値よりserver側の最新データを優先します。
record IDを送る場合は認可も確認する
IDのformatが正しくても、現在の利用者がそのrecordを編集できるとは限りません。IDを変更して他人のrecordへアクセスする攻撃を防ぐには、認証後のuser IDとrecord ownershipをserverで照合します。
$post = findPostById($postId);
if ($post === null) {
http_response_code(404);
exit('記事が見つかりません');
}
if ($post['owner_id'] !== $currentUserId) {
http_response_code(403);
exit('この操作は許可されていません');
}
「hiddenだから利用者はIDを変えない」という前提を置かないでください。format validationとauthorizationは別の確認です。
CSRF tokenをhiddenへ入れる
CSRF対策のsynchronizer token patternでは、serverが予測困難なtokenをsessionへ保存し、同じ値をhidden fieldでformへ含めます。送信時にserver側の値とconstant-time比較します。
form表示時にtokenを生成する
<?php
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrfToken = $_SESSION['csrf_token'];
?>
<form action="update.php" method="post">
<input
type="hidden"
name="csrf_token"
value="<?= htmlspecialchars(
$csrfToken,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
) ?>"
>
<button type="submit">更新する</button>
</form>
受信時にhash_equals()で照合する
<?php
session_start();
$sessionToken = $_SESSION['csrf_token'] ?? '';
$requestToken = $_POST['csrf_token'] ?? '';
if (!is_string($requestToken) ||
$sessionToken === '' ||
!hash_equals($sessionToken, $requestToken)) {
http_response_code(403);
exit('不正なrequestです');
}
hidden fieldそのものが安全だからCSRF対策になるのではありません。攻撃者が予測できないtokenをserver側sessionと照合する設計が重要です。tokenをURLへ含めると履歴・log・Refererへ漏れる可能性があるため、GETではなくstate-changing POST formへ含めます。
SameSite cookie、Origin / Referer確認、framework標準機能なども組み合わせます。独自実装より、使用frameworkのCSRF protectionを優先してください。
sessionの基本はPHP SESSIONの使い方で解説しています。
hidden値を配列として送る
同じnameへ[]を付けるとPHPでarrayになります。
<input type="hidden" name="tag_ids[]" value="2">
<input type="hidden" name="tag_ids[]" value="5">
<input type="hidden" name="tag_ids[]" value="8">
受信側では「arrayであること」と「各要素の型・許可範囲」を確認します。
$rawTagIds = $_POST['tag_ids'] ?? [];
$tagIds = [];
if (is_array($rawTagIds)) {
foreach ($rawTagIds as $value) {
$id = filter_var(
$value,
FILTER_VALIDATE_INT,
['options' => ['min_range' => 1]],
);
if ($id !== false) {
$tagIds[] = $id;
}
}
}
$tagIds = array_values(array_unique($tagIds));
最終的には、現在の利用者が指定可能なtag IDのallowlistと照合します。配列formのname構文はPHPでフォーム値を配列として送る方法を参照してください。
hidden・readonly・disabledの違い
| 指定 | 表示 | 編集 | form送信 |
|---|---|---|---|
type="hidden" |
controlとして非表示 | UIから不可 | nameがあれば送信 |
readonly |
表示 | UIから不可 | 送信 |
disabled |
通常は表示 | 不可 | 送信しない |
CSSでdisplay: none |
非表示 | UIから困難 | enabledでnameがあれば送信 |
どの方法でもHTTP requestを直接作れば値は変更できます。readonlyやdisabledもsecurity境界として信用しません。
GET formでもhidden値は送信できる
<form action="search.php" method="get">
<input type="hidden" name="category" value="php">
<input type="search" name="keyword">
<button type="submit">検索</button>
</form>
送信後のURLは次のようになります。
search.php?category=php&keyword=form
GETではquery stringとしてURLへ出るため、秘密情報やCSRF tokenを含めません。POSTでもbodyが見えにくいだけで秘密にはならず、HTTPSとserver-side validationが必要です。詳しい使い分けはGETとPOSTの違いで解説しています。
$_POSTと$_REQUESTのどちらを使うか
POST formを受けるendpointでは$_POSTまたはfilter_input(INPUT_POST, ...)を使い、入力元を明確にします。
$_REQUESTは設定によりGET・POST・Cookieなど複数sourceを含み得るため、同名keyがあると処理意図が分かりにくくなります。methodも確認します。
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Allow: POST');
http_response_code(405);
exit('POSTで送信してください');
}
method確認だけではCSRF対策やvalidationになりません。各対策を別々に行います。
DB更新では受信keyをallowlist化する
$_POST全体をmodelやSQL updateへそのまま渡すと、攻撃者がhiddenに存在しないkeyも追加できるmass assignmentの危険があります。
$allowed = [
'title' => is_string($_POST['title'] ?? null)
? trim($_POST['title'])
: '',
'body' => is_string($_POST['body'] ?? null)
? trim($_POST['body'])
: '',
];
// $allowedだけをprepared statementへ渡す
is_adminやowner_idがrequestへ追加されても、update対象に入りません。SQLへ値を渡す際はprepared statementを使います。
output時はcontextに合わせてescapeする
hidden値を確認画面へ再表示したり、validation error後にformへ戻したりする場合、外部入力をHTMLへそのまま出力しません。
$value = is_string($_POST['step'] ?? null)
? $_POST['step']
: '';
echo htmlspecialchars(
$value,
ENT_QUOTES | ENT_SUBSTITUTE,
'UTF-8'
);
validationは値が業務ruleに合うかを確認し、escapeはHTML contextでcodeとして解釈されないようにします。目的が違うため両方必要です。
よくあるエラー
Undefined array keyが出る
inputにnameがない、formが未送信、checkboxと同様に条件でfieldが存在しない、違うmethodを見ている可能性があります。$_POST['key'] ?? nullやfilter_input()で存在を確認します。
hidden値が送信されない
nameがあるか、form elementの内側にあるか、controlや祖先のfieldsetがdisabledになっていないかを確認します。
数字を掛けても期待した結果にならない
文字列のまま暗黙変換へ頼らず、整数・小数のformat、範囲、通貨の丸めruleをvalidationします。価格はclient値ではなくserver側のrecordから取得します。
CSRF tokenが一致しない
form表示と送信で同じsessionが継続しているか、session_start()が先に呼ばれているか、複数tab・token更新policyを確認します。framework標準実装があればそのlifecycleへ従います。
hiddenならpasswordやAPI keyを入れてもよいですか?
いけません。HTML source、DevTools、browser extension、network requestから読めます。clientへ送る必要のないsecretはserverだけに保持します。
まとめ
PHPでhidden値を受け取る基本は簡単ですが、hiddenはsecurity機能ではありません。
nameとvalueを設定してformと一緒に送る- 受信値は存在・型・範囲・allowlistをserverで検証する
- 価格・税率・権限・所有者はserver側の値を使う
- record IDはformatだけでなくauthorizationも確認する
- CSRF tokenはsessionなどserver側の値と安全に照合する
- DB更新keyをallowlist化し、output時はescapeする
hiddenは「見せずに送るためのform control」であり、「変更できない値」ではありません。すべてのrequest fieldを攻撃者が変更できる外部入力として扱うことが、安全なPHP formの基本です。