0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

`URL.hostname` は `example.com.` をそのまま返す

0
Posted at

URL.hostname は、ホスト名の末尾に付いたドットを消しません。

new URL('https://example.com/').hostname   // 'example.com'
new URL('https://example.com./').hostname  // 'example.com.'

普段の Web アクセスでは、これで困ることはあまりありません。多くの環境では、example.comexample.com. は同じ相手に届くからです。

ただし、アプリケーション側で hostname をそのまま文字列キーに使うと、見え方が変わります。通信先としては同じように見えても、文字列としては別物だからです。

DNS では、RFC 1034 / RFC 1035 で定義される名前表記の中で、example.com. は root まで書いた完全修飾名として扱われます。

example.com.   ← root まで明示した完全修飾名
example.com    ← 同じ名前を末尾ドットなしで書いた表記

そのため、普段の運用でも次のような場面では末尾ドット付き表記を見かけます。

  • DNS のゾーンファイルで NSCNAME の向き先を書くとき
  • dig example.com. のように完全修飾名として問い合わせたいとき
  • 検索ドメインの影響を避けて「この名前そのもの」を引きたいとき

一方、WHATWG URL Standard の URL は、その表記差を hostname にそのまま残します。

new URL('https://example.com./').hostname  // 'example.com.'

どこで困るのか

いちばん分かりやすいのは、ホスト名をそのままキーに使うときです。

type KvStore = {
  get(key: string): Promise<string | null>;
};

declare const kv: KvStore;

const url = new URL(request.url);
const tenantId = await kv.get(`tenant:${url.hostname}`);

登録側が tenant:shop.example.com で保存していて、参照側が https://shop.example.com./ で来ると、読み出しキーは tenant:shop.example.com. になります。

このずれは、次のような場所で起きやすくなります。

  • KV や Redis のキー
  • DB の検索キー
  • テナント識別
  • URL 正規化後のキャッシュキー

なお、許可リスト照合のようなセキュリティ用途では、末尾ドットだけ見れば十分という話ではありません。IDN、Punycode、ポート、サブドメイン境界なども絡むため、ここでは「キーの取りこぼしを避けるための最低限の正規化」として扱います。

キー用途なら、対策は単純

キーに使う前に、同じ正規化関数を通します。

export function normalizeHostname(hostname: string): string {
  return hostname.trim().toLowerCase().replace(/\.+$/, '');
}

やっていることは 3 つだけです。

  • 前後の空白を除去する
  • 小文字にそろえる
  • 末尾に付いたドットを取り除く

ここでは、防御的に連続する末尾ドットもまとめて落としています。URL.hostname だけを相手にするなら trim() が必要になる場面は多くありませんが、設定画面から受け取る入力にも同じ関数を使うなら入れておくほうが自然です。

一貫してそろえる

登録時と参照時で同じ関数を使います。

async function registerTenant(rawHostname: string, tenantId: string) {
  const hostname = normalizeHostname(rawHostname);
  await kv.put(`tenant:${hostname}`, tenantId);
}

async function handleRequest(request: Request) {
  const url = new URL(request.url);
  const hostname = normalizeHostname(url.hostname);
  const tenantId = await kv.get(`tenant:${hostname}`);
}

1 か所だけ url.hostname をそのまま使う経路が残ると、見えにくい取りこぼしが起きます。

まとめ

URL.hostnameexample.com. をそのまま返します。普段のアクセスでは大きな問題になりにくいのですが、ホスト名を文字列キーとして扱う場面では、example.comexample.com. が別物になります。

DNS では意味のある表記差が、アプリケーションのキー設計にそのまま現れます。

参考情報

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?