CreaTools LogoCreaTools
Tips

PHPでQRコードを生成する前に決めること

2026-01-09

この記事が解決する状況

あなたの状況読むべきセクション
印刷物にQRコードを載せる予定QRコードに何を埋め込むべきか
キャンペーンQRを作りたいID方式のメリット
QRのアクセス数を計測したいサーバー側の処理例
セキュリティが気になるIDの設計 / セキュリティチェックリスト
とにかく実装方法を知りたいライブラリ選定 → 実装パターン

「URLを直書きしたQRを印刷物に載せたことがある」なら、リスクを理解しておくべき。


よくある失敗

「QRコードにURL直書きしたら、キャンペーン終了後もアクセスが来て困った」

印刷物に載せたQRは回収できない。URLを変えたくなっても、QR自体は変わらない。

この手の事故は設計段階で防げる。


QRコードに「何を埋め込むべきか」

URL直埋めのリスク

問題具体例
変更不可キャンペーンURLを差し替えたい → 印刷済みQRは変更不可
失効不可退会した会員のQRが生き続ける
追跡困難どのQRからのアクセスか判別できない
漏洩リスクURL内のパラメータがリファラ経由で外部に漏れる

結論: URL直埋めは「二度と変更しない」確信があるときだけ。

ID方式のメリット

QRには識別子だけを埋め込み、意味付けはサーバー側で行う。

QR内容: https://example.com/q/A1B2C3D4
         └─ IDだけ。行き先はサーバーが決める
要件URL直埋めID方式
リダイレクト先の変更
QRの失効・無効化
アクセス解析・トラッキング
有効期限の設定

IDの設計:推測されない・衝突しない

避けるべきパターン

// ❌ 連番 → 推測可能
$id = $memberId; // 1, 2, 3...

// ❌ タイムスタンプのみ → 衝突リスク
$id = time();

会員ID 1234 のQRを見た攻撃者が 1235 を試す。これで他人の情報にアクセスできたら事故。

推奨パターン

// ✓ UUID v4(ランダム)
$id = bin2hex(random_bytes(16));
// → "a3f8c9e1b2d4f6a8c0e2d4f6a8b0c2e4"

// ✓ HMAC署名付き(改ざん検知可能)
$payload = $memberId . '|' . $expiry;
$signature = hash_hmac('sha256', $payload, $secretKey);
$id = base64url_encode($payload . '|' . $signature);
方式推測耐性改ざん検知用途
UUID v4一般的な識別子
HMAC署名有効期限・権限を含める場合

セキュリティ:攻撃ベクトルと対策

QRコードを悪用した攻撃パターン

攻撃手口対策
ID推測連番を順番に試すUUID v4を使う
QRすり替え正規QRの上に偽QRを貼るドメインを目視確認(ユーザー教育)
URL注入入力値をそのままQRにサニタイズ必須
リファラ漏洩リダイレクト先でURL内パラメータが漏れるReferrer-Policyヘッダー
期限切れQR悪用古いQRが再利用される有効期限チェック

入力値のサニタイズ

ユーザー入力をQRに含める場合は必ずサニタイズ。

// ❌ 危険:入力値をそのまま使用
$content = $_POST['url'];
$qr = (new QRCode())->render($content);

// ✓ 安全:バリデーション + サニタイズ
$url = filter_var($_POST['url'], FILTER_VALIDATE_URL);
if (!$url) {
    throw new InvalidArgumentException('Invalid URL');
}
// さらにドメインをホワイトリストでチェック
$allowedDomains = ['example.com', 'sub.example.com'];
$host = parse_url($url, PHP_URL_HOST);
if (!in_array($host, $allowedDomains, true)) {
    throw new InvalidArgumentException('Domain not allowed');
}
$qr = (new QRCode())->render($url);

サーバー側の処理例

// /q/[id] へのアクセスを処理
<?php
$id = $_GET['id'] ?? '';

// バリデーション
if (!preg_match('/^[a-zA-Z0-9]{32}$/', $id)) {
    http_response_code(400);
    exit('Invalid ID format');
}

// DBから取得
$stmt = $pdo->prepare('SELECT * FROM qr_codes WHERE id = ? AND expires_at > NOW()');
$stmt->execute([$id]);
$record = $stmt->fetch();

if (!$record) {
    http_response_code(404);
    exit('このQRコードは無効です');
}

// アクセスログ記録
$pdo->prepare('INSERT INTO qr_access_logs (qr_id, accessed_at, ip, user_agent) VALUES (?, NOW(), ?, ?)')
    ->execute([$id, $_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT'] ?? '']);

// リファラ漏洩対策
header('Referrer-Policy: no-referrer');

// リダイレクト
header('Location: ' . $record['destination_url']);
exit;

これで「QRの失効」「アクセス解析」「リダイレクト先変更」がすべて可能になる。


ライブラリ選定

ライブラリ特徴判断
chillerlan/php-qrcode軽量・依存少・現行メンテ✓ 採用
endroid/qr-codeSymfony向け・機能豊富フレームワーク利用時
Google Chart API外部依存・非推奨✗ 使わない

なぜ chillerlan/php-qrcode か:

  • 外部APIに依存しない(オフライン生成可能)
  • 依存パッケージが少ない
  • PHP 7.4〜8.x 対応、アクティブにメンテされている
composer require chillerlan/php-qrcode

PHP 7.4以上、ext-gd または ext-imagick が必要。


実装パターン

基本:PNG出力

<?php
require_once 'vendor/autoload.php';

use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;

$id = 'A1B2C3D4'; // DBから取得したID

$options = new QROptions([
    'outputType' => QRCode::OUTPUT_IMAGE_PNG,
    'eccLevel'   => QRCode::ECC_M,
    'scale'      => 10,
]);

header('Content-Type: image/png');
header('Cache-Control: no-store');
echo (new QRCode($options))->render('https://example.com/q/' . $id);

Base64埋め込み(HTML直書き)

<?php
$options = new QROptions([
    'outputType'  => QRCode::OUTPUT_IMAGE_PNG,
    'imageBase64' => true,
    'scale'       => 8,
]);

$base64 = (new QRCode($options))->render('https://example.com/q/' . $id);
?>

<img src="<?= $base64 ?>" alt="QR Code">

外部ファイル不要。メール埋め込みやPDF生成に向く。

SVG出力(印刷向け)

<?php
$options = new QROptions([
    'outputType'    => QRCode::OUTPUT_MARKUP_SVG,
    'svgViewBoxSize' => 100,
]);

header('Content-Type: image/svg+xml');
echo (new QRCode($options))->render('https://example.com/q/' . $id);

拡大しても劣化しない。印刷物・PDF埋め込みに最適。


オプション早見表

オプション説明推奨値
eccLevel誤り訂正レベル通常 ECC_M、ロゴ重ね ECC_H
scale1モジュールのピクセル数8〜12
imageBase64Base64出力HTML埋め込み時 true

eccLevel の選び方:

  • ECC_L(7%): データ量優先
  • ECC_M(15%): 通常用途
  • ECC_H(30%): ロゴ重ね・汚損リスクあり

セキュリティチェックリスト

項目確認
IDは推測困難かUUID v4 または HMAC署名
有効期限はあるかDBに expires_at カラム
失効処理は可能か管理画面から無効化できる
アクセスログは取れるか不正利用の検知に必要
リファラ漏洩対策Referrer-Policy: no-referrer ヘッダー
入力値サニタイズユーザー入力をQRに含める場合
IDフォーマット検証不正な形式のIDを弾く

まとめ

  • QRにはURLではなくIDだけを埋め込む
  • IDは推測不可能な形式(UUID v4 / HMAC)
  • 失効・変更・トラッキングはサーバー側で制御
  • ライブラリは chillerlan/php-qrcode 一択
  • 入力値サニタイズとリファラ漏洩対策を忘れずに

関連記事