テック技報最新号へ →
セキュリティ

SMS OTP をドメイン紐付け形式+one-time-code+WebOTP でフィッシングに強くする

パスキーへ移行するまでの間、SMS の最終行を @ドメイン #コード にし、入力欄に autocomplete="one-time-code"、Android Chrome 向けに WebOTP API を組み合わせると、ワンタップで自動入力でき、別ドメイン(フィッシングサイト)では候補が出なくなる。

@agektmr · 2026-10-16
#authentication#otp#sms#webotp#phishing#2fa#html#passkey

何を解決するか / いつ使うか

  • 2 要素認証・電話番号確認・アカウント復旧・決済確認などで SMS OTP を使っていて、すぐにはパスキーへ移れないとき。
  • アプリを行き来して 6 桁を手入力する離脱と、偽サイトにコードを入力させられるリスクを同時に減らしたいとき。

手順

1. 入力欄

html
<form action="/verify-otp" method="POST">
  <input type="text"
         inputmode="numeric"
         autocomplete="one-time-code"
         pattern="\d{6}"
         required>
</form>
  • type="number" は使わない(増減ボタンが出たり先頭の 0 が消えたりする)。type="tel" も旧来のハックで不要。inputmode="numeric" で数字キーボードにする。
  • autocomplete="one-time-code" で iOS / iPadOS / macOS の Safari がキーボードに OTP を提案する。

2. SMS 本文の最終行をドメイン紐付け形式にする

認証コードは 123456 です

@example.com #123456
  • 最終行に @ +呼び出し元サイトのドメイン、# + OTP。全体は 140 文字以下。
  • Safari(iOS 14 以降)はこの形式なら指定ドメイン上でだけ候補を出す。別ドメインのフィッシングサイトでは自動入力されないので、ユーザーが「いつもと違う」と気づくきっかけになる。

3. WebOTP API(Android の Chrome / Opera / Vivaldi)

js
if ('OTPCredential' in window) {
  window.addEventListener('DOMContentLoaded', () => {
    const input = document.querySelector('input[autocomplete="one-time-code"]');
    if (!input) return;
    const ac = new AbortController();
    const form = input.closest('form');
    form?.addEventListener('submit', () => ac.abort());  // 手入力で送信したら待受を止める
    navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal })
      .then(otp => { input.value = otp.code; form?.submit(); })
      .catch(console.log);
  });
}
  • HTTPS 必須。ユーザーの許可を経てコードが JS に渡される。

注意点

  • これは UX と「別ドメインで使わせない」補助であり、OTP 自体はフィッシング耐性がない(ユーザーが手で偽サイトに打ち込めば通る)。電話番号の再割り当てや SIM 乗っ取りのリスクも残る。本命は パスキーで「詰まない」ための運用のコツ(WebAuthn)。
  • どの機能が使えなくても手入力で送信できるよう、<form> と <input> を基本にしておく。
  • コーディングエージェント向けスキル集「Modern Web Guidance」がこのベストプラクティスに対応した、と @agektmr が紹介している。

出典

X の元の投稿を開く