Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ: Action ViewのサニタイザがHTML Living Standard(旧HTML5)準拠にほか(20230621前編)

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

🔗 ビュー関連

🔗 Action Viewのサニタイザが更新されてHTML Living Standard(旧HTML5)準拠になった

編集部注

このプルリクで言及しているHTML5という仕様は現在廃止されており、WHATWGのHTML Living Standardに置き換わっていますので適宜読み替えてください。サニタイザの名前空間としてはHTML5が使われることになります。

参考: HTML Standard -- html.spec.whatwg.org
参考: HTML5 - Wikipedia
参考: Web Hypertext Application Technology Working Group - Wikipedia

2021年1月28日、WHATWGにあるHTML Review DraftがW3Cによって勧告されたことによってHTML5は廃止された19。このため、2021年5月現在、有効なHTMLの標準規格はHTML Living Standardとなっている20
HTML5 - Wikipediaより抜粋

HTML5標準に準拠したRails::HTML5::Sanitizerのサポートと、Rails 7.1でデフォルト化(サポートされている場合)

Action ViewのHTMLサニタイザをconfig.action_view.sanitizer_vendor設定でコンフィグ可能になった。サポートされる値はRails::HTML4::SanitizerまたはRails::HTML5::Sanitizer

Rails 7.1ではデフォルトでRails::HTML5::Sanitizerに設定され(サポートされている場合)、Rails::HTML4::Sanitizerにフォールバックする。従来のコンフィグではRails::HTML4::Sanitizerがデフォルトになる。
Mike Dalessio
同Changelogより


動機/背景

現代のWebはHTML5で構築されている

Rails 7.0以前で使われているHTMLサニタイザであるrails/rails-html-sanitizerではLoofahとNokogiriが使われており、特にNokogiriのHTML4パーサーであるlibxml2に依存している。

libxml2のHTML4パーサーは、最新のWebアプリケーションが依存するHTML5標準に対応していないため、最新ブラウザと振る舞いが同じでない。これに関する文脈はある程度RFC: Explore alternatives to libxml2 for HTML parsing · Issue #2064 · sparklemotion/nokogiriにあるが、おそらく最も重要なのはlibxml2主要メンテナの以下の発言だろう:

20年以上経過したメンテナンスされていないHTML 4パーサーを最新のWeb向けの入力サニタイズで使うのは、やはりよくないと思う。
https://bugzilla.gnome.org/show_bug.cgi?id=769760#c4より

これによって達成が妨げられるものはほとんどないものの、Railsでセキュリティ問題が発生したり予想外の振る舞いの原因になったりすることがあるかもしれない。

サーバー側でHTML4、クライアント側でHTML5が使われる場合のセキュリティ上の影響

サーバー側のHTML4サニタイザと、クライアント側(ブラウザ)のHTML5パーサーの動作の違いに起因するセキュリティ問題の背景に少し触れておく: loofah/docs/2022-10-decision-on-cdata-nodes.md at main · flavorjones/loofah · GitHub

上のドキュメントで私が書いた以下の文章を引用する:

重要なのは、サニタイズのパスにHTML5パーサーを使えば、この種の問題全体がなくなるということである。

その他の振る舞いの違い
libxml2はHTML5の解析仕様に準拠していないため、サニタイズされたドキュメントが期待通りにならないことがよくある。これは目立たない場合もあるが、アプリケーションの動作に影響を与える場合もある。

アプリケーションの期待と異なる振る舞いの近年の例: Markdown preview and result differ - bug - Discourse Meta

最後の仕上げ

とにかく、このプルリクの前に以下のようなさまざまなことが行われてきている:

サニタイザの他の部分はRailsのHTML5サポートに依存しているので、HTML5を使えるようにRailsをアップデートするときが来たと思う。

詳細

  • Action Viewがrails-html-sanitizer ~> 1.6に依存するようになった
  • mattr_accessor sanitizer_vendorActionView::Helpers::SanitizeHelperに追加された
  • 新たなaction_view.sanitizer_vendorコンフィグの値は、7.0以前ではRails::HTML4::Sanitizerがデフォルトになる
  • 7.1ではaction_view.sanitizer_vendorはデフォルトでRails::HTML5::Sanitizerになる(サポートされている場合)。それ以外の場合はRails::HTML4::Sanitizerにフォールバックする
    同PRより

つっつきボイス:「お、SanitizerがWHATWGのHTML Living Standardに準拠してHTML4HTML5名前空間で指定可能になったのか: HTML4へのフォールバックも可能にしてあるらしい」「rails-html-sanitizerというRailsのgem↓がWHATWG版準拠になったことに対応したみたいですね」「こういうものがあったとは」

rails/rails-html-sanitizer - GitHub

参考: Release 1.6.0 / 2023-05-26 · rails/rails-html-sanitizer

「この間Nokogiri 1.15.0でlibxml2周りのアップデートがあったのも関係しているのかな(ウォッチ20230608)」「ちなみにlibxml2はRuby以外にもいろんなところで使われていますね: PHPのコンパイルオプションにもあったりします」

参考: libxml2 - Wikipedia

「パーサーがHTML4で困ったことは今のところないけど、エッジケースで困る可能性があるのかも」「クライアントはもうWHATWG版に対応済みなんだからサーバー側のパーサーも対応すべきというのはもっともですね👍」

「ところでLoofahってたまに見かけるけど、リポジトリを見るとNokogiriの上に構築されたHTML/XMLパーサーなんですね↓」

flavorjones/loofah - GitHub

🔗 url_forヘルパーに新しいpath_paramsオプションが追加

url_forヘルパーに新しいpath_paramsオプションが追加

これは、ルーティングURLの一部である必須パラメータだけを追加し、それ以外のルーティングについては無関係なクエリパラメータを追加したくない場合に非常に有用。

以下のルーティングがあるとする。

Rails.application.routes.draw do
  scope ":account_id" do
    get "dashboard" => "pages#dashboard", as: :dashboard
    get "search/:term" => "search#search", as: :search
  end
  delete "signout" => "sessions#destroy", as: :signout
end

そしてApplicationControllerが以下のようになっているとする。

  class ApplicationController < ActionController::Base
    def default_url_options
      { path_params: { account_id: "foo" } }
    end
  end

標準のurl_forヘルパーや類似のヘルパーは、以下のように振る舞うようになる。

dashboard_path # => /foo/dashboard
dashboard_path(account_id: "bar") # => /bar/dashboard
signout_path # => /signout
signout_path(account_id: "bar") # => /signout?account_id=bar
signout_path(account_id: "bar", path_params: { account_id: "baz" }) # => /signout?account_id=bar
search_path("quin") # => /foo/search/quin

Jason Meller, Jeremy Beker
同Changelogより


つっつきボイス:「これはルーティング周りの改修」「default_url_optionspath_params: { account_id: "foo" }のように書くと、url_forヘルパーなどが呼ばれるタイミングで、URLの一部になる特定のパラメータについてデフォルト値を指定できる機能らしい」「値が明示的に渡されている場合やクエリパラメータになっている場合は置き換えないんですね」「使い所はすぐには思い浮かばないけど、こういう機能があってもいいと思います👍」

参考: § 4.4 default_url_options -- Action Controller の概要 - Railsガイド

🔗 simple_format:sanitize_optionsが追加

simple_formatヘルパーに追加された:sanitize_optionsで任意のサニタイズオプションを追加できるようになった。

改修前:

  simple_format("<a target=\"_blank\" href=\"http://example.com\">Continue</a>")
  # => "<p><a href=\"http://example.com\">Continue</a></p>"

改修後:

  simple_format("<a target=\"_blank\" href=\"http://example.com\">Continue</a>", {}, { sanitize_options: { attributes: %w[target href] } })
  # => "<p><a target=\"_blank\" href=\"http://example.com\">Continue</a></p>"

Andrei Andriichuk
同Changelogより


つっつきボイス:「simple_formatは改行を含むテキストを概ねそのままの見た目でをHTML化して出力するときなんかによく使いますね: この改修では、たとえばtarget="_blank"を消さずに出力したいときは、たとえば{ sanitize_options: { attributes: %w[target href] }というホワイトリストをサニタイズオプションとして指定できるようになった」「サニタイズオプションは任意のものを指定できるんですね」「Rails 3ぐらいの頃にそれ用のサニタイズメソッドを手作りして回避した覚えがあるけど、sanitize_optionsで渡せるのはありがたい👍」

参考: Rails API simple_format -- ActionView::Helpers::TextHelper

🔗 Active Record関連

🔗 オブジェクト作成時にデータベース側の自動入力属性を割り当て可能にする

このプルリクは、Active Recordの作成ロジックを拡張し、オブジェクト作成時にデータベース側の自動入力された属性が割り当てられるようにする。

以下のスキーマで表されるPostモデルがあるとする。

create_table :posts, id: false do |t|
  t.integer :sequential_number, auto_increment: true
  t.string :title, primary_key: true
  t.string :ruby_on_rails, default: -> { "concat('R', 'o', 'R')" }
end

ただしtitleは主キーとして使われ、テーブルにはシーケンスが入力されるintegerのsequential_numberカラムとruby_on_railsカラムがあり、Postレコードを作成するとsequential_number属性とruby_on_rails属性を入力するデフォルト機能が指定されるようになる:

new_post = Post.create(title: 'My first post')
new_post.sequential_number # => 1
new_post.ruby_on_rails # => 'RoR'

現時点のMySQLとSQLiteのアダプタは、入力されるカラムが1つだけで、そのカラムはauto_incrementでなければならないという制限があるが、PostgreSQLアダプタはRETURNING文で任意の個数の自動入力カラムをサポートしている。

実装の詳細
ソリューション全般としては、追加を可能にするために汎用的かつ拡張可能にすると同時に、現在のRailsが作成時に単一のカラムにしか入力しない(MySQLとSQLiteの場合)という事実を変えないようにする。

  • 最も重要な変更点は、publicなexec_insertinsertメソッドのメソッドシグネチャを両方とも変更したこと。新しく追加するreturningキーワードは、メソッドの戻り値を制御するために、単一の値またはカラムの配列を受け取って、行挿入後に返されるカラム値を定義する。現在のPostgresqlではRETURNINGステートメントの利用のみがサポートされている。returning引数の振る舞いは次のように定義されている: nilはデフォルトの値で、メソッドの振る舞いは変わらない。つまり、引き続き単一の主キーまたはLAST_INSERT_ID()値を返す。カラム名の配列が渡された場合は、配列を返す。ただし現時点では引数でのnil以外の値指定はPostgresqlアダプタでのみサポートされている。
  • ConnectionAdapters::Columnauto_incremented_by_db?という新しいアダプタ抽象属性が追加され、指定のカラムをデータベースでオートインクリメントするかどうかをインクリメント方法にかかわらず指定可能になる。MySQLではauto_incrementに、PostgreSQLではserialにエイリアスする。
  • ConnectionAdapters::Columnにも新しいauto_populated?メソッドが追加され、カラムへの入力方法にかかわらずデータベースで自動入力されたカラムを識別可能になった。現在はauto_incremented_by_dbまたはデフォルトの関数だけをチェックする。
  • SQLite3::Columnに新しいrowidプロパティが追加された。これはintegerカラムをオートインクリメントするための暗黙の方法である。
  • ModelSchema._returning_columns_for_insertが追加され、INSERT後に取得したいカラム値のリストを取得できるようになった。PostgreSQLは自動入力されたすべてのカラムを選択するが、MySQLとSQLiteは auto_incremented_by_db?のみを選択し、暗黙で選択範囲を1つのカラムに限定している。

長期的プラン
この機能は柔軟なので、PostgreSQLアダプター以外に、作成後にカラム値を返せる他のデータベースにも対応する形で拡張可能。
最も近い候補:

sql_for_insertRETURNINGステートメントを構築する方法をアダプタに知らせる必要がある。

rails/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb at aa162cefe19b81daa5d7f8c2b9ee4fe2564e73fc · rails/rails

def sql_for_insert(sql, _pk, binds, _returning) 
   [sql, binds] 

そしてreturn_value_after_insert?メソッドを拡張すれば、単にINSERT後にオートインクリメントを返す以上の処理も行える。

rails/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb at aa162cefe19b81daa5d7f8c2b9ee4fe2564e73fc · rails/rails

 def return_value_after_insert?(column) 
   column.auto_populated? 

RETURNINGステートメントやその他の代替手段をサポートしないデータベースでは、自動入力されたカラムの値を取得するために追加のSELECTクエリを実行する必要がある。クエリを増やすのはアプリケーションにとって望ましくないので、振る舞いをコンフィグ可能にすべきだが、さらに複雑になる。今回一気にすべてを改修しない理由のひとつ。
同PRより


つっつきボイス:「Changelogによると、主にDB側のオートインクリメントをActive Record側で自動反映するのに使える機能らしい↓」「auto_populated?_returning_columns_for_insertというメソッドも追加されるんですね」

Active Recordでのレコード作成時に自動生成されるカラムを割り当てる
レコード作成ロジックを変更することで、auto_incrementカラムをレコード作成時に割り当て可能にする。これによって、モデルの主キーへのリレーションにかかわらず、レコード作成直後にauto_incrementカラムを割り当てられるようになる。
この変更によるメリットが最も大きいのはPostgreSQLアダプタで、RETURNINGステートメントを利用するレコードのINSERT後に、任意の個数の自動生成カラムをオブジェクトに割り当てられるようになる。
Nikita Vasilevsky
同Changelogより

「プルリクにも書かれているように、PostgreSQLだとRETURNINGで複数の値を返せる機能を利用できるけど、MySQLやSQLite3だとINSERT後に改めてSELECTしないと2つ目以降の自動セットされた値を返せないんですよ」「クエリが増えるというのはそういう意味なんですね」

参考: PostgreSQL 15ドキュメント 6.4. 更新された行のデータを返す -- RETURNING

🔗 connected_toshardsハッシュの最初のキーをdefault_shardで使うようになった

アプリケーションによっては、コネクションモデルのシャード名に:defaultを使いたくない場合がある。残念なことに、Active Recordはプールマネージャから正しいコネクションを得るために何らかのシャードを仮定しなければならないため、:defaultシャードの存在を期待する。

アプリケーションで手動設定する代わりに、connects_toがシャードのハッシュからデフォルトシャード名を推測して、最初のシャードをデフォルトであると仮定するようになる。
たとえば以下のようなモデルがあるとする。

class ShardRecord < ApplicationRecord
  self.abstract_class = true
  connects_to shards: {
    shard_one: { writing: :shard_one },
    shard_two: { writing: :shard_two }
  }

これで、このクラスのdefault_shardshard_oneに設定されるようになる。

修正: #45390
Eileen M. Uchitelle
同Changelogより


つっつきボイス:「シャード名を:defaultにしたくない場合があるというのはわかるけど、1番目のシャードを暗黙でデフォルトに使うのはあまり嬉しくないかも」「ドキュメントにも追記されているけど↓、ちょっと気になりますね」

最初のシャード名は必ずしもdefaultにしなければならないわけではありません。Railsはconnects_toのハッシュにある最初のシャード名を「デフォルト」のコネクションと仮定します。このコネクションは、内部で型データなどの情報を読み出すのに使われます(スキーマはシャード間で同じとします)。
同PRより

🔗 Action Cable関連

🔗 Action Cableに単独のヘルスチェックが追加された

ヘルスチェックRackアプリを指定のパスにマウントするhealth_check_pathhealth_check_application設定が追加された。
Action Cableを単独でマウントする場合に有用。
Joé Dupuis
同PRより


つっつきボイス:「少し前にHealthControllerがRailsに導入されましたけど(ウォッチ20230207)、Action Cableでもヘルスチェックエントリポイントを設定可能になったんですね」「ヘルスチェックのために実装しなくて済むの助かります」「実装し忘れに気づいたところから作業が始まりがちですよね」

# actioncable/test/server/health_check_test.rb#21
  test "no health check app are mounted by default" do
    get "/up"
    assert_equal 404, response.first
  end

  test "setting health_check_path mount the configured health check application" do
    @server.config.health_check_path = "/up"
    get "/up"

    assert_equal 200, response.first
    assert_equal "Hello world!", response.last
  end

🔗 Active Storage関連

🔗 ActiveStorageの添付ファイルをフォームのPOSTで削除可能になった

既に添付ファイルは、以下のようにnilに更新することで削除可能。

User.find(params[:id]).update!(avatar: nil)

ただしフォームではnil paramはPOSTできず、空文字列しかPOSTできない。しかし空文字列をPOSTすると署名済みblob idとして扱われるため、ActiveSupport::MessageVerifier::InvalidSignature: mismatched digestエラーが発生する。

この変更でnilと空文字列が削除として扱われるようになり、以下のように添付ファイルを削除可能になった。

User.find(params[:id]).update!(params.require(:user).permit(:avatar))

Nate Matykiewicz
同Changelogより


つっつきボイス:「フォームから空文字列""を渡した場合にも添付ファイルを削除できるようにしたんですね」「そうそう、HTMLフォームではnilをPOSTできない」「コメントによるとblank?だと遅いので== ""にしたとありますね」

# activestorage/lib/active_storage/attached/model.rb#64
          def #{name}=(attachable)
            attachment_changes["#{name}"] =
-             if attachable.nil?
+             if attachable.nil? || attachable == ""
                ActiveStorage::Attached::Changes::DeleteOne.new("#{name}", self)
              else
                ActiveStorage::Attached::Changes::CreateOne.new("#{name}", self, attachable)
              end
          end

🔗 設定関連

🔗 ビジュアルエディタ設定用のVISUAL環境変数を追加

エディタを開くのに使う環境変数としてVISUALをサポートし、EDITORよりも優先されるようになった。
Summer ☀️
同Changelogより


つっつきボイス:「ビジュアルエディタを使えるVISUAL環境変数を追加してEDITORより優先するようになるのか」「このあたりの環境変数はrailsコマンドでエディタが開くときのデフォルトエディタを指定するんですよね」

動機/背景

credentialsencryptedsecretsコマンドで一時ファイルを開いて好みのエディタで編集するのに使われるRails::Command::Helpers::Editorモジュールでは、現在Editor環境変数しか使えない。VISUAL環境変数も利用可能にして、EDITORよりも優先すべきである。

StackExchangeのこの回答を参照:

Editorで指定するエディタは、「高度な」端末機能を使わなくても動作可能にすべき(古いedviexモードのように)。これはテレタイプ端末で使われていた。

VISUALには、viemacsのようなフルスクリーンエディタを指定可能。

例: bashでエディタを起動するとき(C-x C-eを使う)、bashはまずVISUALで指定されたエディタを試し、VISUALが失敗した場合(端末がフルスクリーンエディタをサポートしていない場合)はEDITORを試す。

これで、EDITORは未設定のままでもvi -eなどを設定してもよいようになる。

詳細

このプルリクは、Rails::Command::Helpers::Editorを変更して、最初にVISUALに値があるかどうかを調べ、VISUALが空の場合はEDITORの値を調べる。

追加情報

Ruby on Rails Discussionsの提案(#82928)を参照。


前編は以上です。

バックナンバー(2023年度第2四半期)

週刊Railsウォッチ: RailsでApplication Layer Encryption、rubocop-factory_bot登場ほか(20230614後編)

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

Publickey

publickey_banner_captured


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。