短い間隔で続くWordPressの修正版から見える狙われどころ

つくば市のホームページ制作会社

10月6日、WordPress 7.1.3が公開されました。セキュリティ修正7件と不具合修正4件を含む保守版で、公式は早めの更新を勧めています。7.1系では、9月17日の7.1.1、9月22日の7.1.2に続いて、ひと月足らずのあいだに三度目のセキュリティ修正となりました。

間隔がこれだけ詰まると、正直なところ「また更新か」と感じます。ただ、三つの修正版の中身を横に並べてみると、どこが繰り返し狙われているのか、運用のどこを見直せばよいのかがかなり具体的に見えてきます。今回は公式の発表と各版の変更内容をもとに、中小企業のサイトを預かる立場から読み解いてみます。

九月半ばから続いた修正版の流れ

まず時系列を整理しておきます。9月17日の7.1.1は大きめの保守版で、セキュリティ修正11件に加え、コアの不具合修正17件、ブロックエディターの不具合修正21件を含んでいました。報告されたセキュリティ上の問題の多くは、ログインしたユーザーによる操作が前提のものです。

その5日後、9月22日に出た7.1.2は修正1件だけの版でした。ただしその1件は、ページテンプレートの解決処理におけるパストラバーサルで、認証なしで狙え、条件次第ではリモートからのコード実行につながるという重いものです。変更されたファイルも wp-includes/template.php ひとつに絞られており、急ぎで出された修正だったことがうかがえます。

そして10月6日の7.1.3です。今回は1件ごとの深刻さよりも、管理画面、書き出し、埋め込み、問い合わせ処理と、触れている範囲の広さが目立ちます。

7.1.3で直された箇所

公式の発表に挙がっている修正は次のとおりです。

  • コメント管理画面での保存型XSS。承認待ちのコメントを経由して悪用できるもので、Trail of Bitsの研究者が報告
  • WP_Http::make_absolute_url() メソッドでのサービス妨害(DoS)
  • WordPress標準の書き出し機能(WXR形式)での二次的なSQLインジェクション
  • 投稿者(Author)権限のユーザーが記事を先頭に固定できてしまう弱点
  • 非公開や未公開の投稿に付いたコメントが、認証なしで見えてしまう問題。Patchstackの研究者が報告
  • Imgurの埋め込みでのXSS
  • {status}_{type} フックに渡る引数を偽装でき、アクション名の衝突につながる問題。WordPressのセキュリティチームが報告

変更されたファイルの一覧には、wp-admin/js/common.js、wp-admin/includes/export.php、REST APIの投稿コントローラー、カスタマイザー関連、class-wp-http.php、class-wp-oembed.php、class-wp-query.php、post.php が並びます。どれも日常的に動いている部品で、特定の設定をしているサイトだけの話ではないと考えておくほうが安全です。

コメント欄はずっと入口であり続けている

三つの版を並べて最初に気づくのは、コメントまわりの修正が何度も出てくることです。7.1.3では、承認待ちコメントを経由した管理画面での保存型XSSと、非公開投稿のコメントが認証なしで見える問題が直されました。さかのぼると7.1.1でも、段落の整形処理を経由した認証不要の保存型XSS(コメントの承認が条件)や、ログインしたユーザーなら誰でもコメントの親子関係を付け替えられる問題が修正されています。

ここで押さえておきたいのは、「承認しなければ表に出ないから安全」とは言い切れないという点です。報告の説明を読むかぎり、承認前のコメントが管理画面に表示される段階が関わっています。つまりスパムの山を確認するために管理画面を開く、その日常の作業が攻撃の起点になりうるわけです。

実務の面では、コメント機能をそもそも使っていないサイトなら、設定でコメントの受け付けを止めておくのがいちばん手堅い対策です。会社案内が中心の中小企業サイトでは、テーマの初期設定のままコメント欄が開いていて、誰も読まないスパムが溜まり続けているという例を今でもよく見かけます。使っているサイトでも、古い記事のコメント受け付けを一定期間で閉じる設定は検討する価値があります。

権限の低いアカウントと書き出し機能

二つ目の傾向は、投稿者や寄稿者(Contributor)といった、権限の低いアカウントを前提にした問題が続いていることです。7.1.3の「投稿者が記事を先頭に固定できる」件は影響こそ限られますが、7.1.1では寄稿者以上の権限で任意の投稿を上書きできる問題や、下書きと承認待ち記事のスラッグが寄稿者以上に見えてしまう問題が直されています。REST APIのテンプレート処理での、認証済みユーザーによるパストラバーサルも同じ版に含まれていました。

外部のライターに寄稿者アカウントを発行している、退職した担当者のアカウントが残っている、といった状況は珍しくありません。こうしたアカウントのパスワードが漏れれば、権限が低くても修正前の穴を突く足がかりになります。更新とあわせて、使われていないアカウントを整理しておくと安心です。

もうひとつ見落としやすいのが、書き出し機能での二次的なSQLインジェクションです。二次的というのは、先にデータベースへ保存された値が、あとで別の処理に使われる段階で問題を起こす型を指します。WXR形式の書き出しは、サーバー移行やサイトの複製、バックアップの代わりとして制作会社がよく使う機能です。普段は誰も触らない機能でも、移行作業のときに一斉に使われる、という点は意識しておきたいところです。

古い系統への移植と「支援の対象は最新版だけ」

WordPressには、修正を古いメジャー版にも移植するという慣例があります。7.1.1のときは、7.0から4.7までのすべての系統に修正版が出ました。発表によると、7.0から6.7までは11件すべての影響を受け、系統が古くなるにつれて影響を受ける件数は減り、4.7では6件だったとされています。7.1.2の1件は、4.7までのすべての系統が影響を受けていました。4.6以前には、もうセキュリティ修正は届きません。

7.1.3についても、修正は厚意として、セキュリティ修正の対象となっている4.7までの系統に必要に応じて移植され、準備ができ次第出していくと説明されています。ここで公式が毎回添えているのが、「積極的に支援しているのは最新版だけ」という一文です。古い系統に修正が届くのはあくまで厚意であり、不具合修正までは届きません。

PHPの版や、更新の止まったプラグインとの相性を理由に、古い系統に据え置いているサイトは今もあります。移植版が出ている以上、すぐに危険というわけではありませんが、最新版より遅れて届く可能性があることや、修正の範囲がセキュリティに限られることは、据え置きを選ぶときの前提として持っておくべきでしょう。

続く修正版を受け止める段取り

では、短い間隔で届く修正版にどう付き合えばよいのでしょうか。制作会社として保守を受け持つ立場から、確認しておきたい点を挙げます。

まず、マイナー版の自動更新が有効になっているかどうかです。公式の発表でも、バックグラウンドでの自動更新に対応しているサイトでは更新が自動で始まるとされています。自動更新を止めて手作業で管理している場合は、修正版の公開から適用までに何日かかっているかを一度振り返ってみてください。7.1.2のように認証なしで狙える重い修正が出たとき、その日数がそのまま無防備な期間になります。

次に、更新後に何を確かめるかです。今回の7.1.3は、管理画面のスクリプト、書き出し、カスタマイザー、埋め込み、投稿の問い合わせ処理に手が入っています。ステージング環境がある場合は、管理画面のコメント一覧、カスタマイザーでの表示確認、外部サービスの埋め込みを含むページ、記事一覧の並び順あたりを見ておくと、思わぬ表示崩れに早く気づけます。

そして、更新とは別に続けたいのが、コメント機能とアカウントの棚卸しです。三つの版を通して、コメントと低い権限のアカウントが繰り返し入口になっていることを考えると、使っていない機能を閉じ、使っていない人のアカウントを消すことは、修正版を待つ以外にこちらから打てる数少ない手立てです。

修正版が続くのは落ち着かないものですが、裏を返せば、外部の研究者やセキュリティ企業からの報告が受け止められ、数十人規模の協力で修正が形になっている表れでもあります。7.1.3の発表には、修正に関わった人の名前がずらりと並んでいました。届いた修正を淡々と当て、そのたびに中身へ少し目を通しておく。地味ですが、それがいちばん確かな付き合い方だと考えています。

Service Workerを起こさずに通信を振り分ける仕組みがSafariにも届いた

つくば市のホームページ制作会社

web.devが10月2日に公開した9月分のウェブプラットフォーム新機能まとめに、地味ながら気になる項目がありました。Safari 27が、Service Workerの「Static Routing API」に対応したという話です。Chromeでは版123から使えていた機能で、これでSafariとChrome系の両方で動くようになりました。表示速度に関わる仕組みなので、少し掘り下げて紹介します。

Service Workerが抱えていた待ち時間

Service Workerは、ページとネットワークの間に入って通信を仲介するスクリプトです。オフライン表示やキャッシュの制御に使われ、WordPressでもPWA化やキャッシュ系のプラグインを通じて、知らないうちに組み込まれているサイトがあります。

ただ、Service Workerを置くと、そのサイトへのリクエストはいったんService Workerのfetchイベントを通るのが基本です。Service Workerが止まっている状態なら、まず起動させてから処理を始めることになります。結果として、キャッシュから返すだけ、あるいはそのままネットワークへ流すだけのリクエストでも、Service Workerの起動を待つ時間が生じていました。

Chrome for Developersの解説によると、Static Routing APIはこの部分を解消するためのものです。どのパスをどこから取得するかをあらかじめ宣言しておくことで、ブラウザはキャッシュやネットワークから取るだけのためにService Workerを動かさずに済むとされています。web.devの記事でも、起動を待たずに取得できることでページ遷移の速度が上がると説明されています。

インストール時に道筋を登録しておく

使い方は、Service Workerのinstallイベントの中で event.addRoutes() を呼び、振り分けのルールを渡すだけです。ルールは「条件」と「取得元」の組み合わせで書きます。

条件には、URLの形(urlPattern)、リクエストのメソッド、モード、取得先の種類、Service Workerが起動中かどうか、を指定できます。複数の条件を書いた場合は、すべてを満たしたときだけそのルールが使われます。取得元は、ネットワーク、キャッシュ、従来どおりのfetchイベント、ネットワークとfetchイベントを競わせる方式、の中から選びます。名前を指定して特定のキャッシュストレージを使うこともできます。

たとえば、フォームへのPOST送信はService Workerを通さず、そのままネットワークへ送る、という指定は次のように書けます。

addEventListener('install', (event) => {
  event.addRoutes({
    condition: {
      urlPattern: "/form/*",
      requestMethod: "post"
    },
    source: "network"
  });
});

画像の拡張子を条件にして特定のキャッシュから返す、記事ページはService Workerが起動中のときだけfetchイベントに回す、といった書き分けも解説の例に挙がっています。なお、試験提供の段階では registerRouter() という一度しか呼べないメソッドでしたが、利用者の意見を受けて今の addRoutes() に変わったそうです。Chromeの開発者ツールでは、Applicationパネルに登録済みのルールが表示され、Networkパネルでもルールに一致したリクエストを見分けられます。

制作現場で押さえておきたいこと

web.devの対応表を見ると、ChromeとEdgeは版123から、Safariは今回の27から対応していますが、Firefoxはまだ未対応です。対応していないブラウザでは従来どおりfetchイベントで処理されるだけなので、ルールを追加しても表示が壊れるわけではありません。速くなる環境が増えた、と受け止めるのが妥当なところです。

中小企業のサイトで直接この記述を書く場面はそう多くないかもしれません。それでも、Service Workerを使っているサイトを引き継いだときや、キャッシュ系のプラグインを選ぶときに、こうした振り分けに対応しているかどうかは一つの見どころになります。特にお問い合わせフォームや管理画面への通信のように、キャッシュを挟む意味がないリクエストを素通しにできるのは、速度だけでなく不具合の切り分けの面でも助かります。

Service Workerは入れると速くなる一方で、仲介が増えるぶんの負担もある、という両面を持つ仕組みでした。その負担を宣言ひとつで減らせる手段が主要なブラウザにそろってきたので、手元のサイトでService Workerが何を受け持っているのか、一度確かめてみる良い機会だと思います。

更新のたびに横へずれる数字はCSSの指定ひとつで止まる

つくば市のホームページ制作会社

料金表や集計の画面を作ったとき、データもレイアウトも正しいのに、なぜか数字の並びが落ち着かないと感じたことはないでしょうか。金額の列の右端がそろわない、カウンターの値が変わるたびに行全体が少し横に揺れる。海外のフロントエンド制作者が公開している記事で、この見慣れた違和感の正体と直し方があらためて整理されていました。

原因はCSSでもデータでもなく、フォントが標準で出してくる数字の形にあるそうです。多くのフォントでは数字が文字と同じように幅を変えて並び、1は0より細く詰められます。文章の中ではこれが自然ですが、縦にそろえたい表や、その場で値が切り替わる表示では、桁の幅が毎回変わるため列が崩れたり数字が踊ったりします。

フォントに眠っている別の数字を呼び出す

解決策は font-variant-numeric: tabular-nums; の指定です。MDNの解説によると、これはOpenTypeの tnum に対応する値で、すべての数字を同じ幅にそろえます。字形そのものは変わらず、同じ大きさの枠に一つずつ収まるので、金額の列は右端がきれいに並び、タイマーや在庫数のように刻々と変わる値も横に動かなくなります。記事の著者は、表や数値のパネル、カウンターに入る数字にはとりあえず付けておく、という使い方を勧めていました。

同じ記事では、ほかの切り替えも紹介されています。文章中の数字を小文字のように上下に出入りさせて行になじませる oldstyle-nums、「1/2」と打った文字を本物の分数の形にする diagonal-fractions、ラベルを太さを保ったまま小さな大文字にする font-variant-caps: small-caps です。分数の指定は日付のような斜線の入った数字まで巻き込むので、分数の部分だけを span で囲んで適用するのがよい、という注意も添えられていました。

ブラウザの対応についての心配はほとんどありません。MDNでは font-variant-numeric は2020年1月から主要ブラウザで使える状態とされています。一方で落とし穴もあり、著者によるとGoogle Fontsで配信される可変フォントの多くは、容量を減らすために旧来の数字の形や分数などを省いているとのことです。指定しても見た目が変わらないときは書き方の誤りではなく、フォントのファイルにその字形が入っていない可能性があります。字形が無い場合も普通の数字で表示されるだけなので、表示が壊れることはありません。

日本語のサイトでは、本文の数字が和文フォントの字形で表示されることも多く、フォントによって効き方が変わります。中小企業のサイトでも、料金表や営業時間、実績の数値を並べたページは意外と多いものです。見出しや本文を変えずに数字の部分だけ整えられるので、既存サイトの手直しのついでに、使っているフォントで tabular-nums が効くかを一度確かめておくと、表まわりの印象が少し締まるはずです。

リンクの下線の長さをCSSだけで整えられるようになった

つくば市のホームページ制作会社

9月22日に安定版が出たChromeの新版には、見た目の小さな調整に効く機能がいくつか入りました。先日このブログで取り上げた、埋め込みのiframeが中身に合わせて伸びる仕組みもそのひとつですが、今回はその陰に隠れがちなCSSの追加に目を向けます。中心になるのは、下線や打ち消し線の始まりと終わりの位置を調整できる text-decoration-inset というプロパティです。あわせて、スクロールで切り替わる表示につける目印の振る舞いを選べるようになった変更と、ポップオーバーやダイアログの閉じ方の改善も紹介します。

下線の端を内側にも外側にもずらせる

これまで、文字につける下線(underline)や上線、打ち消し線は、表示された文字列とまったく同じ長さで引かれていました。デザインの都合で「下線を少し短くしたい」「左右に少しはみ出させたい」と思っても、text-decoration 側にはその手段がありませんでした。

Chrome for Developersの解説によると、text-decoration-inset は線の始点と終点を文字列の端から調整するためのプロパティです。値には auto、長さ、パーセンテージを指定でき、ひとつの値で両端をまとめて、ふたつの値で始点と終点を別々に指定できます。たとえば次のように書くと、両端が10pxずつ内側に寄ります。

a {
  text-decoration: underline;
  text-decoration-inset: 10px;
}

正の値は線を内側に縮め、負の値は外側に伸ばします。始点と終点に違う値を入れれば、線を左右どちらかに寄せることもできます。見出しの下に短いアクセント線を引く、ボタン風のリンクで線の両端を少し余らせる、といった細かな調整が、線そのものの設定だけで済むようになります。

背景のグラデーションで描いていた下線の置き換え

制作の現場では、マウスを乗せたときに下線が左から伸びてくる演出がよく使われています。従来は、本物の下線ではなく background-image のグラデーションを細く敷いてその幅を動かしたり、::after の疑似要素で線を作ってアニメーションさせたりするのが定番でした。

公式の解説でも、このプロパティを使えば背景のグラデーションや追加の要素に頼らず、文字の装飾線そのもので「線が現れる」効果を作れると説明されています。本物の下線のまま扱えるので、線の太さや色、文字からの距離といった既存の指定と組み合わせやすく、複数行にまたがるリンクでも装飾線として素直に振る舞うことが期待できます。

一方で、今回の情報源はChrome側の発表で、ほかのブラウザでの対応状況は別に確かめる必要があります。対応していないブラウザでは通常の長さの下線になるだけなので、見た目の上乗せとして段階的に取り入れるのが無理のない使い方でしょう。既存サイトの疑似要素による実装を急いで書き換えるより、新しく作るパーツから試すほうが安全です。

スクロールの目印をタブとしても扱える

もうひとつのCSSの変更は、横スクロールのカルーセルなどで使う scroll-marker-group の拡張です。このプロパティは、スクロール領域の前後に目印のまとまりを自動で作るもので、新版ではその振る舞いを links と tabs のふたつから選べるようになりました。

リリースノートによると、既定の links では目印のまとまりがナビゲーション、個々の目印がリンクとして扱われ、Tabキーで順に移動できます。tabs を選ぶと、まとまりがタブの一覧、目印がタブ、中身がタブパネルとして扱われ、Tabキーで止まるのは選択中の目印だけになり、目印の間は矢印キーで移動します。選ばれていないパネルの中身は支援技術からも隠れます。

タブ切り替えのUIを自前で作ると、キー操作や役割の付与まで含めた実装はそれなりに手間がかかります。見た目だけでなく操作の約束事までCSSの指定で選べるのは、アクセシビリティの面でも意味のある変更だと感じます。ただしこちらも対応ブラウザは限られるため、当面はJavaScriptで作った既存のタブを置き換えるより、検証環境で挙動を確かめておく段階だと考えています。

外側を押すと閉じる動作の取りこぼしが減る

同じ版では、ポップオーバーやダイアログの外側をクリックして閉じる動作も見直されました。判定に使うイベントが、押した瞬間と離した瞬間の組み合わせから、クリックのイベントに変わっています。これにより、タッチ画面でのスクロール操作や右クリックで、意図せずポップオーバーやダイアログが閉じてしまうことがなくなるとされています。

スマートフォンでメニューを開いたまま中身をスクロールしようとして閉じてしまう、という問い合わせは小さな会社のサイトでも起こりがちです。ブラウザ側の変更なので制作側の作業は要りませんが、標準のポップオーバーやダイアログを使っている部分は、今後こうした改善が自動的に届く側に回ります。下線の長さのような細部も含めて、手作りしていた部分を少しずつ標準の機能に寄せていくと、保守の手間が軽くなっていくはずです。

消しても戻るWordPressの裏口は片付ける順番が肝心

つくば市のホームページ制作会社

改ざんされたWordPressサイトで、怪しいファイルを消したのに数秒後には元どおり、という経験がある制作者は少なくないと思います。セキュリティ企業のSucuriが、まさにその状態を作り出すマルウェアの分析結果をブログで公開しました。注入されたコードに残っていた目印から「SC」と呼ばれています。

Sucuriによると、SCは同じ裏口を少なくとも8か所に分けて置いています。.user.ini の auto_prepend_file で毎リクエストの前に読み込まれるローダー、wp-content 直下の db.php と advanced-cache.php というドロップイン、有効なテーマの functions.php の末尾に足されたブロック、そしてキャッシュ系に見せかけた偽プラグインが mu-plugins と通常の plugins の両方に置かれていたそうです。どれか一つでも残っていれば、ほかの全部を書き戻す作りになっています。

やっかいなのはファイル以外の置き場所です。本体はオプションテーブルの行にも圧縮して保存され、サーバーの共有メモリにも書き込まれていたとされています。さらにWordPressのcronに再配置用のフックが登録され、関連する亜種ではデータベースのトリガーで管理者アカウントを作り直す例もあったとのことです。ファイルを完璧に掃除しても、次のアクセスでデータベースや共有メモリから全部よみがえるわけです。

削除より先に止める

裏口としての働きも一通りそろっています。プラグイン一覧や更新チェックから自分を隠し、ユーザー一覧に出ない管理者を作り、セキュリティ系プラグインを無効化して削除する指示も受け取れます。指示はブロックチェーンの公開ゲートウェイ約20か所を経由して読み取る方式で、一つを遮断しても残りが使われます。ネットショップでは決済画面に不正なスクリプトを差し込まれるおそれもあるとされています。

Sucuriが示す片付け方の要点は「順番」です。まず auto_prepend_file の読み込み先を無害な中身に置き換えてから設定を外します。PHPがこの値を最大300秒キャッシュするため、先に読み込み先を消すとアカウント内のPHPがすべてエラーになるからです。次にデータベースの行、共有メモリ、cron、トリガー、隠れた管理者といったファイル以外の複製を消し、最後にファイルを一度に片付けます。テーマの functions.php は、目印で囲まれた部分だけを削ってテーマ本来のコードは残します。

中小企業のサイトを預かる立場で覚えておきたいのは、消したファイルが戻ってきたら「掃除が終わっていない合図」と受け取ることです。同じファイルを何度も消すより、データベースやcron、ユーザーテーブルに目を向けるほうが近道になります。共有サーバーでは共有メモリをホスティング会社にしか消せない場合もあるので、早めに相談できる窓口を確かめておくと安心です。侵入のきっかけは既知の脆弱性が多いとされているので、更新を止めないことと、復旧後に関係するパスワードをすべて変えることもあわせて進めたいところです。

埋め込みのiframeが中身の高さに合わせて伸びる仕組み

つくば市のホームページ制作会社

予約フォームやコメント欄、外部サービスの口コミ表示など、サイトに別ページを埋め込む場面ではいまも iframe がよく使われます。そのたびに悩まされるのが高さの問題です。中身が長くなると枠の中にスクロールバーが出てしまい、かといって大きめの固定値にすると下に不自然な余白が残ります。Chrome 154 では、この高さ合わせをブラウザ側に任せられる仕組みが入りました。

これまでの高さ合わせは手作業だった

iframe は既定では自分専用の表示領域を持ちます。埋め込まれた文書がその領域より縦に長ければ、利用者には枠の中にもう一本スクロールバーが見えることになります。ページ全体のスクロールと枠内のスクロールが二重になり、スマートフォンでは特に操作しにくくなります。

これを避けるため、制作現場では埋め込まれた側で中身の高さを測り、postMessage() で親ページへ送り、親ページ側の JavaScript で iframe の高さを書き換える、という手順を組むのが定番でした。Chrome for Developers の解説でも、今回の機能はこのよくある実装を置き換えるものと位置づけられています。外部サービスが配布する埋め込みコードに、高さ調整用のスクリプトが一緒に付いてくるのもこのためです。

この方法は動きますが、送る側と受け取る側の両方にコードが要り、送信元の確認を怠ると関係のないページからのメッセージで高さを変えられてしまうおそれもあります。小さなフォームひとつのために毎回これを書くのは、正直なところ手間でした。

親ページと埋め込み側の双方で宣言する

新しい仕組みでは、親ページ側で iframe に frame-sizing という CSS プロパティを指定します。縦方向に伸ばしたい場合は次のように書きます。

.embed {
  width: 100%;
  frame-sizing: content-height;
}

ただし、親ページが指定するだけでは有効になりません。埋め込まれる文書の側も、head に専用の meta 要素を置いて「中身の大きさに合わせてよい」と同意する必要があります。

<meta name="responsive-embedded-sizing" content="allow-origins=*">

frame-sizing には、従来どおりの動きを保つ auto のほか、content-width、content-height、content-inline-size、content-block-size が用意されています。横書きのサイトで縦に伸ばしたい埋め込みなら、content-height か content-block-size を使う場面がほとんどになりそうです。max-height: 80vh のような既存の CSS の制約と組み合わせることもできるので、長くなりすぎる中身に上限を設けておくこともできます。

中身が後から変わるときの扱い

ブラウザは埋め込み先のレイアウト変更を常に監視しているわけではありません。「もっと見る」でコメントを追加読み込みしたり、折りたたみを開いたりして中身が変わったときは、埋め込まれた側で window.requestResize() を呼んで計算し直しを求めます。解説によると、文書の変更をひととおり済ませてから呼ぶのがよいとされています。枠の大きさが変わると表示領域が変わり、それが中身を変え、また大きさが変わる、という繰り返しを避けるために、あえて明示的に呼ぶ形にしているそうです。

別ドメインの埋め込みでは許可先を絞る

この仕組みは、別ドメインから読み込む iframe でも使えます。その場合、埋め込まれる側の meta 要素で、高さ合わせを許す親ページのオリジンを指定できます。

<meta name="responsive-embedded-sizing" content="allow-origins=https://publisher.example">

複数のオリジンを許す場合は空白で区切って並べます。解説では、外部向けのウィジェットでは必要なオリジンだけに絞るよう勧めています。また、この指定はどのサイトが自分のページを埋め込めるかを決める Content Security Policy の frame-ancestors とは役割が別で、それを補う位置づけとされています。埋め込みを許すかどうかと、中身の大きさを親に伝えてよいかどうかは、分けて考えるということです。

自社で予約フォームや問い合わせフォームを別システムで動かし、複数のサイトに埋め込んでいるような構成なら、allow-origins=* で済ませず、実際に埋め込むドメインを書いておくのが無難です。

今の実装を残したまま試せる

現時点でこの機能を使えるのは Chrome 154 以降で、他のブラウザでの対応はこれからです。そのため、すぐに従来の高さ調整スクリプトを外すことはできません。解説では、@supports を使って対応ブラウザだけで新しい指定に切り替える書き方が紹介されています。

.widget {
  width: 100%;
  height: 500px;
}

@supports (frame-sizing: content-height) {
  .widget {
    height: auto;
    frame-sizing: content-height;
  }
}

埋め込まれた側でも、"requestResize" in window で有無を確かめてから呼べば、未対応のブラウザでエラーになることはありません。既存の postMessage() による調整を代替手段として残しつつ、対応ブラウザでは新しい仕組みに任せる、という段階的な移行がしやすい設計です。

WordPress で外部フォームや予約システムを埋め込んでいるサイトでは、埋め込み元のサービスがこの meta 要素に対応しないと親ページ側だけでは使えません。自社で埋め込み用のページを用意している場合は、まずそちらに meta 要素を入れて Chrome で挙動を見ておくと、他のブラウザが追いついたときにスクリプトを外す判断がしやすくなりそうです。iframe の高さという地味ながら長年の手当てが必要だった部分が、標準の書き方に置き換わっていく流れとして、覚えておいて損はないと思います。

innerHTMLに代わるHTMLの差し込み方がそろってきた

つくば市のホームページ制作会社

JavaScriptで画面の一部を書き換えるとき、多くの現場ではいまも innerHTML に文字列を代入する書き方が使われています。手軽で、どのブラウザでも動き、昔のサンプルコードもほとんどがこの形です。ところがこの秋、この「HTMLの文字列を差し込む」という基本動作のまわりで、ブラウザ側の整備が一段進みました。

9月16日にベータ版となったChrome 155では、HTMLを差し込む新しいメソッド群と、差し込みながら少しずつ流し込むストリーミング用のメソッドが入っています。Firefoxは今年2月の148で、差し込むときに危険な部分を取り除く setHTML() を先に出荷しました。安全性と速さという別々の課題に、同じ入り口から手が入りつつある、というのが今回の見立てです。

innerHTMLが抱えてきた二つの弱点

innerHTML の弱点は、大きく分けて二つあります。一つ目は安全性です。ユーザーが入力した文字列や外部から取ってきたデータをそのまま代入すると、紛れ込んだイベント属性などから任意のスクリプトが動く、いわゆるクロスサイトスクリプティング(XSS)の入り口になります。Mozillaの解説では、XSSは10年近くにわたってウェブの脆弱性の上位三つに入り続けているとされています。

二つ目は、同じような目的のメソッドが乱立していることです。Chromeのドキュメントでは、innerHTML、outerHTML、insertAdjacentHTML、createContextualFragment、setHTMLUnsafe などを並べたうえで、上書きなのか追記なのか、危険なタグを取り除くのか、スクリプトは実行されるのか、といった違いを即答できる開発者は少ないだろうと率直に書いています。確かに、制作の現場でもこの違いを意識して使い分けている例はあまり見かけません。

さらにもう一点、これらはすべて「差し込むHTMLが最初から全部そろっている」ことが前提です。サーバーから届く途中のHTMLを順に表示していく、というHTML本来の強みを、JavaScriptからの差し込みでは活かせませんでした。

危険な部分を取り除いてから入れるsetHTML

安全性の側の答えが、HTML Sanitizer APIと、その中心にある setHTML() です。使い方は innerHTML への代入をメソッド呼び出しに置き換えるだけで、差し込む前にブラウザが危険な要素と属性を取り除きます。MDNによると、既定の設定では script、iframe、embed、object などの要素や、onclick のようなイベント属性がすべて落とされ、さらにクリックジャッキングやなりすましに使われうる要素、コメント、data- 属性まで取り除かれます。

許可する要素を自分で指定することもできます。たとえば段落と太字だけを通す、といった「許可リスト型」の設定を作れば、想定外のタグは一切入りません。MDNは、危険なものを列挙して外す方式より、必要なものだけを列挙して通す方式のほうが安全だと説明しています。将来新しい危険な要素が見つかっても、許可リストに入っていなければ最初から通らないためです。

これまでこの役割は、DOMPurifyのような外部ライブラリが担ってきました。MDNは、ブラウザに組み込まれた方式のほうが、解析の文脈や実行されうるコードをより正確に把握できるため、安全な方のメソッドを使うなら外部ライブラリは不要になるという立場です。ただし、この機能はまだ主要ブラウザすべてでは使えない段階にあり、特にSafariが未対応である点は押さえておく必要があります。

名前をそろえた差し込み用メソッド群

Chrome 155で入るのは、差し込む位置ごとに名前をそろえたメソッド群です。要素の中身を置き換える setHTML() を軸に、要素ごと置き換える replaceWithHTML()、前後に足す beforeHTML() と afterHTML()、子要素の先頭と末尾に足す prependHTML() と appendHTML() が用意されています。ベータ版の告知では、これらが insertAdjacentHTML() の実質的な置き換えになると位置づけられています。

それぞれには、危険な部分の除去をしない「Unsafe」付きの版もあります。名前だけ見ると使ってはいけないもののようですが、Chromeのドキュメントは、これは入力の信頼度に応じてリスクを意識してほしいという注意書きであり、使うなという意味ではないと説明しています。Unsafe版では、オプションを指定すれば差し込んだHTML内のスクリプトを実行させることもでき、初期値は実行しない設定です。

地味な変更に見えますが、名前の規則が一つにまとまることの効果は小さくありません。コードを読んだ人が「このメソッドは安全側か」「どこに入るか」を名前だけで判断できるようになれば、レビューや引き継ぎのときの見落としが減ります。複数の人が触るサイトほど、こうした読みやすさは効いてきます。

届いた分から順に流し込むストリーミング

もう一つの柱が、ストリーミング用のメソッドです。上の各メソッドには streamHTML() や streamAppendHTML() のような対応版があり、Streams APIの書き込み口を返します。fetch() で取ってきた応答をそのままつなげば、サーバーから届いた分から順に画面へ反映できます。Chrome 151からは、応答を文字列の流れとして取り出す textStream() という便利なメソッドも加わり、中間の変換処理を書かずに済むようになりました。

これまで、ページの一部をJavaScriptで差し替える作りでは、応答をすべて受け取ってから一気に入れるのが普通でした。Chromeのドキュメントは、単一ページアプリケーションが最初の読み込み以外ではHTMLの逐次表示の恩恵を受けられないことを大きな弱点として挙げ、この新しいメソッドでそこを埋められるとしています。共通のフッターのような部品を別ファイルから取り込み、キャッシュを効かせる使い方も例に挙がっています。ただし、JavaScriptが動かないと表示されない以上、最初の画面で見える部分に使うのは避けるべきだという注意も添えられています。

同じ取り組みの一部として、HTMLの中で後から中身を差し替える仕組みも進んでいます。場所の目印を置いておき、ページの後半で template 要素に for 属性を付けて中身を送ると、目印の位置にはめ込まれる、という方式です。重い処理を待つ部分だけを後回しにして、先に骨組みを表示できます。こちらはChrome 150から使えるとされています。

ブラウザごとの足並みと当面の付き合い方

ここまでの話を整理すると、各ブラウザの足並みはまだそろっていません。setHTML() による安全な差し込みはFirefoxが先行し、Chromeは差し込み位置ごとのメソッドとストリーミングを前に進めています。一方でSafariはSanitizer APIに対応しておらず、MDNでも主要ブラウザで広く使える段階にはないと表示されています。

Chromeのチームは、新しいメソッドの形をそのまま使えるようにする補助ライブラリをnpmで公開しています。ただしドキュメント自身が、この補助ライブラリは実際には流し込まず、全部受け取ってからまとめて差し込むと明記しています。安全な差し込みの部分も、ブラウザ側のSanitizer APIがなければ成り立ちません。つまり補助ライブラリは、書き方を先取りするためのもので、機能そのものを全ブラウザで再現するものではない、と理解しておくのがよさそうです。

もう一つの論点は、Trusted Typesとの関係です。これは危険になりうる差し込み口に、決められた変換処理を必ず通させる仕組みです。Mozillaは、setHTML() を採用したあとなら、setHTML() だけを許して他の危険な差し込み方を禁じる厳しい設定にしやすくなり、今後の書き間違いによるXSSの再発を防げると説明しています。Content Security Policyが大がかりな改修を必要としたため広く普及しきらなかった、という反省も同じ記事で触れられており、今回の仕組みは「コードの置き換えを小さく済ませる」ことに重きが置かれています。

中小企業のサイト運用で見ておきたいところ

では、日々のサイト制作や保守で何をすればよいか。すぐに全面的な書き換えが必要な話ではありません。ただ、お知らせの一覧をJavaScriptで読み込んで表示する部分や、問い合わせフォームの入力内容を確認画面に出す部分など、外から来た文字列を innerHTML で差し込んでいる箇所は、どのサイトにも意外と残っています。まずはそうした箇所を把握しておき、Safariを含む主要ブラウザで使えるようになった段階で、setHTML() への置き換えを検討する、という順番が現実的です。

WordPressのサイトでも、テーマやプラグインのJavaScriptに同じような書き方が含まれていることがあります。自社で書いた追加のスクリプトであれば、置き換えの候補として一覧にしておくだけでも、将来の改修の見積もりがしやすくなります。外部のプラグインについては、作者側の対応を待つことになりますが、どの部分が外から来たHTMLを扱っているかを知っておくことには意味があります。

ストリーミングのほうは、表示速度を気にする規模の大きなサイトや、画面の一部を頻繁に差し替える管理画面のような作りで先に効いてくるはずです。普通のコーポレートサイトでは急いで取り入れる理由は少ないものの、HTMLを後から流し込めるようになると、ページの組み立て方の選択肢が確実に増えます。長く制作を続けてきた立場から見ても、innerHTML ひとつで何でも済ませていた時代から、目的に応じて安全な道具を選べる時代へと、基本の部分が静かに入れ替わり始めているように感じます。