フォームを送信する際に、ユーザーには見せずにデータを添付したい場面があります。CSRF トークン・セッション ID・選択中のタブ状態・Ajax でのページ番号など、様々なシーンで type="hidden" の input 要素が活躍します。
この記事では、JavaScript で hidden input を動的に追加する方法を3つのアプローチで解説し、それぞれの違いと使い分け、そして innerHTML を使う際の XSS リスクも含めて実践的に説明します。
type=”hidden” の input とは何か
type="hidden" の input 要素は、画面上には表示されませんが、フォーム送信時にその name と value がサーバーに送信されます。ページのレンダリングには影響せず、CSS でスタイルを当てても意味がありません。
| 用途 | 例 |
|---|---|
| CSRF トークン | サーバー側で生成したトークンを埋め込み、改ざん防止 |
| ID・外部キー | 選択した商品 ID・ユーザー ID などを非表示で送信 |
| 状態の保持 | 現在のページ番号・タブ・並び順を送信時に付与 |
| Ajax 追加パラメータ | 送信直前に JavaScript で動的に値をセット |
3つのアプローチの比較
| 方法 | 既存 DOM への影響 | XSS リスク | 推奨度 |
|---|---|---|---|
| createElement + setAttribute | なし | なし | ◎ 最推奨 |
| insertAdjacentHTML | なし | 値をエスケープすれば安全 | ○ 可 |
| innerHTML += | 既存要素が再レンダリングされる | あり(要注意) | △ 非推奨 |
方法1:createElement + setAttribute(最推奨)
DOM 操作の基本かつ最も安全な方法です。要素を JavaScript オブジェクトとして生成・設定してからフォームに追加するため、ユーザー入力値をそのまま扱っても XSS が発生しません。
<form id="myForm"> <input type="text" name="username"> <button type="submit">送信</button> </form>
const form = document.getElementById('myForm');
// input 要素を生成して属性をセット
const hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'token';
hidden.value = 'abc123xyz';
// フォームに追加
form.appendChild(hidden);
hidden.setAttribute("type", "hidden") と hidden.type = "hidden" はどちらも動作します。プロパティへの直接代入の方がタイプ数が少なく、IDE の補完も効くため一般的です。既存の hidden input を更新する(重複追加の防止)
フォーム送信ボタンを複数回クリックしたとき、毎回新しい hidden input が追加されると同じ name が重複してしまいます。追加前に存在チェックを行い、あれば値を更新するだけにするのが安全です。
function setHiddenField(form, name, value) {
let input = form.querySelector(`input[name='${name}']`);
if (!input) {
input = document.createElement('input');
input.type = 'hidden';
input.name = name;
form.appendChild(input);
}
input.value = value; // 既存なら値だけ更新
}
// 使い方
setHiddenField(form, 'page', '3');
setHiddenField(form, 'page', '4'); // 重複せず値が 4 に更新される
複数の hidden input を一括追加(DocumentFragment 活用)
function addHiddenFields(form, fields) {
const fragment = document.createDocumentFragment();
Object.entries(fields).forEach(([name, value]) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = String(value);
fragment.appendChild(input);
});
form.appendChild(fragment); // DOM 更新は1回だけ
}
// 使い方
addHiddenFields(form, {
utm_source: 'google',
utm_medium: 'cpc',
referrer: document.referrer,
});
方法2:insertAdjacentHTML(開発者が制御する固定文字列のみOK)
insertAdjacentHTML は HTML 文字列を指定位置に挿入するメソッドです。innerHTML += と異なり既存の DOM を破壊しないため、こちらのほうが安全です。
ただし安全なのは、ソースコードに直接書いた固定文字列を使う場合のみです。ユーザー入力・API レスポンス・URL パラメータなど外部から来る値を value に使う場合は、後述のエスケープ関数が必要です。
form.insertAdjacentHTML('beforeend',
'<input type="hidden" name="token" value="abc123xyz">');
以下のように動的な値をそのまま埋め込むと XSS の原因になります。
// BAD: userInput に " name="evil" value="x" が入ると属性インジェクションが起きる
const userInput = getSomeValue();
form.insertAdjacentHTML('beforeend',
`<input type="hidden" name="q" value="${userInput}">`);
function escapeAttr(str) {
return String(str)
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>');
}
form.insertAdjacentHTML('beforeend',
`<input type="hidden" name="q" value="${escapeAttr(userInput)}">`);
方法3:innerHTML +=(非推奨・理由を理解する)
innerHTML += はコードが短く書けますが、2つの深刻な問題があるため原則として使わないことを推奨します。
問題1:既存の DOM が再レンダリングされる
innerHTML += は内部で「既存 HTML を文字列化 → 新しい HTML を結合 → 全体を再パース」という処理を行います。そのため、フォーム内のテキスト入力値・フォーカス・イベントリスナーがすべてリセットされます。
const form = document.getElementById('myForm');
const nameInput = form.querySelector('[name="username"]');
nameInput.addEventListener('input', () => console.log('入力中'));
// ユーザーが入力したあとに...
form.innerHTML += '<input type="hidden" name="token" value="x">';
// ↑ この時点で username の入力値・イベントリスナーが消える
問題2:XSS リスク
value に外部からの入力値を使うと、悪意のある文字列が HTML として解釈されスクリプトが実行される危険があります。
// 本当に使う場合は先述の escapeAttr() でエスケープする // ただし、この用途には createElement を使うほうがはるかに安全で明快
実践パターン集
フォーム送信直前に値を追加・更新する
submit イベントをフックして、送信直前に動的な値をセットするパターンです。
const form = document.getElementById('myForm');
form.addEventListener('submit', (e) => {
// 送信時刻を付与
setHiddenField(form, 'submitted_at', new Date().toISOString());
// 現在のページ URL を付与
setHiddenField(form, 'source_url', location.href);
});
function setHiddenField(form, name, value) {
let input = form.querySelector(`input[name='${name}']`);
if (!input) {
input = document.createElement('input');
input.type = 'hidden';
input.name = name;
form.appendChild(input);
}
input.value = value;
}
チェックボックスの選択 ID をまとめて hidden input に入れて送信
複数チェックボックスの選択値を1つの hidden input にまとめてカンマ区切りで送信するパターンです。
<form id="deleteForm"> <input type="checkbox" class="item-cb" value="101"> 商品A <input type="checkbox" class="item-cb" value="102"> 商品B <input type="checkbox" class="item-cb" value="103"> 商品C <button type="submit">選択を削除</button> </form>
const form = document.getElementById('deleteForm');
form.addEventListener('submit', (e) => {
const checked = [...form.querySelectorAll('.item-cb:checked')]
.map(cb => cb.value);
if (checked.length === 0) {
e.preventDefault();
alert('削除する項目を選択してください');
return;
}
setHiddenField(form, 'delete_ids', checked.join(','));
});
FormData で hidden input を含む全フィールドを収集して Ajax 送信
FormData オブジェクトはフォーム内のすべての input(hidden を含む)を自動的に収集します。動的に追加した hidden input も確実に含まれます。
form.addEventListener('submit', async (e) => {
e.preventDefault();
// 送信前に hidden field を追加
setHiddenField(form, 'action', 'create');
// FormData が hidden input も自動収集
const formData = new FormData(form);
// デバッグ: 送信データを確認
for (const [key, value] of formData.entries()) {
console.log(key, value);
}
try {
const res = await fetch('/api/submit', {
method: 'POST',
body: formData, // Content-Type は自動で multipart/form-data
});
const result = await res.json();
console.log('送信成功:', result);
} catch (err) {
console.error('送信失敗:', err);
}
});
FormData の詳しい使い方は【JavaScript】FormDataの使い方|Ajaxでフォーム送信を簡単にする方法をご覧ください。
PHP での受け取り方
hidden input で送信された値は、通常の input と同じく $_POST または $_GET で受け取れます。
以下は2つのパターンを示しています。①は単純な値の受け取り、②は hidden input を利用した CSRF トークン検証の例です(CSRF 対策には hash_equals() でタイミング攻撃を防ぐ比較が必要です)。
// ① 通常の hidden input 値の受け取り
$pageNo = (int)($_POST['page'] ?? 1);
$sortOrder = $_POST['sort_order'] ?? 'asc';
// ② CSRF トークン検証(hidden input を CSRF 対策に使う場合の例)
// セッションに保存したトークンと比較する
$submittedToken = $_POST['csrf_token'] ?? '';
$expectedToken = $_SESSION['csrf_token'] ?? '';
// hash_equals() でタイミング攻撃を防ぎながら比較
if (empty($submittedToken) || !hash_equals($expectedToken, $submittedToken)) {
http_response_code(403);
exit('不正なリクエストです');
}
PHP でのフォーム値の受け取りは【PHP】フォーム送信された値を受け取る方法も参考にしてください。
hidden input の値を読み取る・更新する・削除する
const form = document.getElementById('myForm');
// 読み取り(name 属性で取得)
const tokenInput = form.querySelector('input[name="token"]');
console.log(tokenInput?.value);
// 更新
if (tokenInput) tokenInput.value = 'newToken456';
// 削除
tokenInput?.remove();
// 全ての hidden input を取得
const allHidden = form.querySelectorAll('input[type="hidden"]');
allHidden.forEach(el => console.log(el.name, el.value));
querySelector の使い方は【JavaScript】querySelector の使い方完全ガイドで詳しく解説しています。
よくある質問
innerHTML += はフォーム内の HTML を一度文字列に変換して再パースするため、既存の入力値・イベントリスナーがすべてリセットされます。createElement か insertAdjacentHTML('beforeend', ...) を使ってください。$_POST["name"] は最後の値のみ取れます(配列にするには name="items[]" 形式が必要)。JavaScript の FormData は getAll("name") で全値を取得できます。意図せず重複した場合は本記事で紹介した setHiddenField() で値の上書きのみにする方法を使ってください。FormData.append(name, value) で直接追加できます。フォーム要素を経由せず JavaScript オブジェクトを JSON で送る場合は JSON.stringify() + Content-Type: application/json で送信する方法が一般的です。まとめ
JavaScript で hidden input を動的に追加する方法を3つ解説しました。選び方は明確です。
- 動的な値を扱うなら
createElement(XSS リスクなし・最推奨) - 静的な HTML を短く書くなら
insertAdjacentHTML(既存 DOM を壊さない) innerHTML +=は使わない(入力値リセット・XSS リスク・再レンダリングコスト)
また、送信直前に値をセットする setHiddenField() ユーティリティと、FormData による Ajax 送信パターンをセットで覚えておくと実務で即使えます。
フォーム送信時のさらなる活用として、【JavaScript】フォーム送信時にローディングアニメーションを表示する方法も合わせてご覧ください。