- Ruby / Rails関連
週刊Railsウォッチ: Action ViewのサニタイザがHTML Living Standard(旧HTML5)準拠にほか(20230621前編)
こんにちは、hachi8833です。
🔗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
最後の仕上げ
とにかく、このプルリクの前に以下のようなさまざまなことが行われてきている:
- 2021: NokogiriのHTML5サポート追加 - Epic: merge Nokogumbo into Nokogiri · Issue #2204 · sparklemotion/nokogiri
- 2022: Loofahが継承されるよう
Nokogiri::HTML5
サブクラスを修正 - html5 subclassing by flavorjones · Pull Request #2534 · sparklemotion/nokogiri- 2023: Loofah 2.21でHTML5をサポート - Release 2.21.0 / 2023-05-10 · flavorjones/loofah
- 今朝早く: rails-html-sanitizer 1.6.0.rc2でHTML5サニタイザをサポート - Comparing v1.5.0...v1.6.0.rc2 · rails/rails-html-sanitizer
サニタイザの他の部分はRailsのHTML5サポートに依存しているので、HTML5を使えるようにRailsをアップデートするときが来たと思う。
詳細
- Action Viewが
rails-html-sanitizer ~> 1.6
に依存するようになったmattr_accessor
sanitizer_vendor
がActionView::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に準拠してHTML4
とHTML5
名前空間で指定可能になったのか: HTML4へのフォールバックも可能にしてあるらしい」「rails-html-sanitizerというRailsのgem↓がWHATWG版準拠になったことに対応したみたいですね」「こういうものがあったとは」
参考: Release 1.6.0 / 2023-05-26 · rails/rails-html-sanitizer
「この間Nokogiri 1.15.0でlibxml2周りのアップデートがあったのも関係しているのかな(ウォッチ20230608)」「ちなみにlibxml2はRuby以外にもいろんなところで使われていますね: PHPのコンパイルオプションにもあったりします」
「パーサーがHTML4で困ったことは今のところないけど、エッジケースで困る可能性があるのかも」「クライアントはもうWHATWG版に対応済みなんだからサーバー側のパーサーも対応すべきというのはもっともですね👍」
「ところでLoofahってたまに見かけるけど、リポジトリを見るとNokogiriの上に構築されたHTML/XMLパーサーなんですね↓」
🔗 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_options
でpath_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_insert
とinsert
メソッドのメソッドシグネチャを両方とも変更したこと。新しく追加するreturning
キーワードは、メソッドの戻り値を制御するために、単一の値またはカラムの配列を受け取って、行挿入後に返されるカラム値を定義する。現在のPostgresqlではRETURNING
ステートメントの利用のみがサポートされている。returning
引数の振る舞いは次のように定義されている:nil
はデフォルトの値で、メソッドの振る舞いは変わらない。つまり、引き続き単一の主キーまたはLAST_INSERT_ID()
値を返す。カラム名の配列が渡された場合は、配列を返す。ただし現時点では引数でのnil
以外の値指定はPostgresqlアダプタでのみサポートされている。ConnectionAdapters::Column
にauto_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_insert
でRETURNING
ステートメントを構築する方法をアダプタに知らせる必要がある。def sql_for_insert(sql, _pk, binds, _returning) [sql, binds]
そして
return_value_after_insert?
メソッドを拡張すれば、単にINSERT後にオートインクリメントを返す以上の処理も行える。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_to
のshards
ハッシュの最初のキーを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_shard
がshard_one
に設定されるようになる。修正: #45390
Eileen M. Uchitelle
同Changelogより
つっつきボイス:「シャード名を:default
にしたくない場合があるというのはわかるけど、1番目のシャードを暗黙でデフォルトに使うのはあまり嬉しくないかも」「ドキュメントにも追記されているけど↓、ちょっと気になりますね」
最初のシャード名は必ずしも
default
にしなければならないわけではありません。Railsはconnects_to
のハッシュにある最初のシャード名を「デフォルト」のコネクションと仮定します。このコネクションは、内部で型データなどの情報を読み出すのに使われます(スキーマはシャード間で同じとします)。
同PRより
🔗 Action Cable関連
🔗 Action Cableに単独のヘルスチェックが追加された
ヘルスチェックRackアプリを指定のパスにマウントする
health_check_path
とhealth_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
コマンドでエディタが開くときのデフォルトエディタを指定するんですよね」
動機/背景
credentials
、encrypted
、secrets
コマンドで一時ファイルを開いて好みのエディタで編集するのに使われるRails::Command::Helpers::Editor
モジュールでは、現在Editor
環境変数しか使えない。VISUAL
環境変数も利用可能にして、EDITOR
よりも優先すべきである。
Editor
で指定するエディタは、「高度な」端末機能を使わなくても動作可能にすべき(古いed
やvi
のex
モードのように)。これはテレタイプ端末で使われていた。
VISUAL
には、vi
やemacs
のようなフルスクリーンエディタを指定可能。例: bashでエディタを起動するとき(
C-x C-e
を使う)、bashはまずVISUAL
で指定されたエディタを試し、VISUAL
が失敗した場合(端末がフルスクリーンエディタをサポートしていない場合)はEDITOR
を試す。これで、
EDITOR
は未設定のままでもvi -e
などを設定してもよいようになる。詳細
このプルリクは、
Rails::Command::Helpers::Editor
を変更して、最初にVISUAL
に値があるかどうかを調べ、VISUAL
が空の場合はEDITOR
の値を調べる。追加情報
前編は以上です。
バックナンバー(2023年度第2四半期)
週刊Railsウォッチ: RailsでApplication Layer Encryption、rubocop-factory_bot登場ほか(20230614後編)
- 20230613前編 Arel::Nodes::Cteが追加、html_escape_onceの修正ほか
- 20230608後編 Jets v4リリース、頑張らない型導入、Rust言語からCrabがforkほか
- 20230607前編 MessagePackがcookieシリアライザとメッセージシリアライザにも導入ほか
- 20230531後編Rubyで環境変数を扱う、Web標準に「Baseline」ステータス追加ほか
- 20230525後編 Ruby 3.3.0-preview1リリース、in_order_ofのバグ修正ほか
- 20230524前編 withで作成したリレーションをjoinsで指定可能に、キャッシュストアの例外処理を統一ほか
- 20230502 スライド『Rails 7.1をn倍速くした話』、Rails 7.1でMessagePackをサポートほか
- 20230427後編 第1回Rails Worldが10月に開催、『研鑽Rubyプログラミング』でRuby本体も高速化ほか
- 20230425前編 Rails 7.1の複合主キー対応が引き続き進む、exceptメソッドにwithoutエイリアスが追加ほか
- 20230413後編 ShopifyのRubyパーサーyarp、RJITを書いた理由ほか
- 20230412前編 複合主キーの実装が進む、Rails公式のバグ再現用テンプレートほか
- 20230406後編 Rubyオブジェクトモデルクイズの最難問ほか
- 20230405前編 Arel::Nodes::NodeにAPIドキュメントが追加、rubocop-mdほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)