- Ruby / Rails関連
週刊Railsウォッチ: sanitize_sql_likeは重要、X-XSS-Protectionヘッダーのデフォルト変更、kredis gemほか(20211206前編)
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
今回は以下の2つの公式更新情報から見繕いました。
- Automated shard swapping middleware, standardised error reporting interface and more! | Riding Rails
- Composable blobs, improved upsert and much more! | Riding Rails
🔗 SQLログで属性をフィルタする機能が追加
SQLログで属性をフィルタする
従来のSQLクエリのログでは
ActiveRecord::Base.filter_attributes
がフィルタされていなかった。
今回、prepared_statement
が有効になっていれば属性が[FILTERED]
でフィルタされるようになった。
# 改修前
Foo Load (0.2ms) SELECT "foos".* FROM "foos" WHERE "foos"."passw" = ? LIMIT ? [["passw", "hello"], ["LIMIT", 1]]
# 改修後
Foo Load (0.5ms) SELECT "foos".* FROM "foos" WHERE "foos"."passw" = ? LIMIT ? [["passw", "[FILTERED]"], ["LIMIT", 1]]
Aishwarya Subramanian
同Changelogより
つっつきボイス:「お、filter_attributes
でフィルタをかけたときにSQLログでも属性がフィルタされるようになったのね」「これは賢い」「SQLログはログレベルを:debug
にしないと出ませんが、これはやっておくべき👍」
参考: 3.1 Rails全般の設定 -- Rails アプリケーションを設定する - Railsガイド
🔗 Active Storageの改修2点
- PR: Add ActiveStorage::Blob.compose by gmcgibbon · Pull Request #41544 · rails/rails
- PR: [ActiveStorage] Custom Metadata by joshuamsager · Pull Request #43294 · rails/rails
つっつきボイス:「1個目の#41544は、GCS(Google Cloud Storage)のcompose
機能に対応したのか」「GCSのcompose
はファイルを結合する機能なのね↓」「GCSのcompose
機能を使うと、たとえばログファイルを手元に持ってきて結合したりせずにGCS上でAPIを使って結合したりできるのか、へ〜!」「そんな機能があったんですね」
参考: Cloud Storage | Google Cloud
参考: GCS上のファイルを結合する - Qiita
「ファイル変更を見るとAWS S3やMicrosoft Azureにも追加されてますね」「どれどれ、S3やAzureの場合はeach
で回して結合している↓」「S3だとIO.copy_stream
で持ってきたものをeach
でそのまま結合しているのね」「Azureも似たような感じ」
#
def compose(*source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
object_for(destination_key).upload_stream(
content_type: content_type,
content_disposition: content_disposition,
part_size: MINIMUM_UPLOAD_PART_SIZE,
metadata: custom_metadata,
**upload_options
) do |out|
source_keys.each do |source_key|
stream(source_key) do |chunk|
IO.copy_stream(StringIO.new(chunk), out)
end
end
end
end
# activestorage/lib/active_storage/service/azure_storage_service.rb#110
def compose(*source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
client.create_append_blob(
container,
destination_key,
content_type: content_type,
content_disposition: content_disposition,
metadata: custom_metadata,
).tap do |blob|
source_keys.each do |source_key|
stream(source_key) do |chunk|
client.append_blob_block(container, blob.name, chunk)
end
end
end
end
「GCSの場合は専用のbucket.compose
を使ってGCS上で結合できるからシンプル↓」「GCSはローカルに持ってこなくていいので効率よさそう👍」
# activestorage/lib/active_storage/service/gcs_service.rb#137
def compose(*source_keys, destination_key, filename: nil, content_type: nil, disposition: nil, custom_metadata: {})
bucket.compose(source_keys, destination_key).update do |file|
file.content_type = content_type
file.content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
file.metadata = custom_metadata
end
end
「2つ目の#43294は、Active Storageが作るメタデータ以外に"custom"
のような任意構造のメタデータも追加できるようになった↓」「これは使いたい人いそう」「今までなかったんですね」
# activestorage/test/models/blob_test.rb#L269
+ test "updating the metadata updates service metadata" do
+ blob = directly_upload_file_blob(filename: "racecar.jpg", content_type: "application/octet-stream")
+
+ expected_arguments = [
+ blob.key,
+ {
+ content_type: "application/octet-stream",
+ disposition: :attachment,
+ filename: blob.filename,
+ custom_metadatata: { "test" => true }
+ }
+ ]
+
+ assert_called_with(blob.service, :update_metadata, expected_arguments) do
+ blob.update!(metadata: { custom: { "test" => true } })
+ end
+ end
🔗 upsert_all
にupdate_only:
オプションが追加
upsert_all
メソッドに:update_only
オプションが追加され、コンフリクト時に更新するカラムのリストを指定できるようになった。
従来は、:on_duplicate:
オプションで更新用のSQL文をカスタマイズするしかなかった。今回:update_only:
オプションが追加されたことで、コンフリクト時に更新したいカラムのリストを渡せるようになった。
Commodity.upsert_all(
[
{ id: 2, name: "Copper", price: 4.84 },
{ id: 4, name: "Gold", price: 1380.87 },
{ id: 6, name: "Aluminium", price: 0.35 }
],
update_only: [:price] # Only prices will be updated
)
同Changelogより
つっつきボイス:「upsert_all
にupdate_only:
オプションが追加されて、更新時に更新するカラムを指定できるようになった🎉」「上で言うと、レコードが既に存在する場合はprice
だけが更新されるということですね」
「テストコードでは、upsert_all
を2回実行して、2回目だけupdate_only: :name
を指定すると、name
だけ更新されてisbn
は更新されない↓」「レコードが存在しなかった場合は今までどおり全部のカラムがINSERTされるんですね、なるほど理解です」「テストコードを見れば振る舞いがわかるようになっていてありがたい: ドキュメントだけだと振る舞いがわかりにくいときはテストコードも見るのがおすすめです」
# activerecord/test/cases/insert_all_test.rb#L345
+ def test_upsert_all_only_updates_the_column_provided_via_update_only
+ Book.upsert_all [{ id: 101, name: "Perelandra", author_id: 7, isbn: "1974522598" }]
+ Book.upsert_all [{ id: 101, name: "Perelandra 2", author_id: 7, isbn: "111111" }], update_only: :name
+
+ book = Book.find(101)
+ assert_equal "Perelandra 2", book.name, "Should have updated the name"
+ assert_equal "1974522598", book.isbn, "Should not have updated the isbn"
+ end
🔗 Pathname#existence
が追加
つっつきボイス:「Active Supportにexistence
メソッドが入った↓」
# activesupport/lib/active_support/core_ext/pathname/existence.rb#18
def existence
self if exist?
end
「なるほど、Active SupportのObject#presence
みたいなチェックをファイルの存在について行ってチェインできるのか↓」「ファイルが存在すればそのファイル自身を返して、存在しなければnil
を返すんでしょうね」「後置if
だとチェックと処理が分かれてしまうけど、existence&.read
なら簡潔に書き下せるのがいいですね👍」
# 同PRより
content = Rails.root.join('file').read if Rails.root.join('file').exist?
↓
content = Rails.root.join('file').existence&.read
- Rails API:
Object#presence
「Object#presence
みたいな便利メソッドは昔から好きなので、いつかRuby本家に入ってくれたら嬉しいけど、Railsのpresent?
やblank?
あたりの仕様がRuby側で議論になりそうな気もするので難しいかも」「議論になりそうなのわかります」「Pathname#existence
ならRubyに入っても大丈夫そう」
- Rails API:
Object#present?
- Rails API:
Object#blank?
🔗 Rails.error.handle
にフォールバック値を指定できるようになった
handle
にオプションでfallback:
パラメータを渡せるようにし、raiseがハンドリングされたときにこの値が返されるようにした。これにはnull objectやデフォルト値、またはnil
返しと異なる何らかのフラグが使える。
user = Rails.error.handle(fallback: User.anonymous) do
User.find_by(params)
end
同PRより抜粋
つっつきボイス:「これは前回入ったRails.error
への機能追加ですね(ウォッチ20211129)」「フォールバック値を指定する機能はたしかに欲しい」
🔗 コネクションプールでrole
やshard
も指定可能に
つっつきボイス:「マルチプルデータベース関連のプルリクが多い@eileencodesさんによるものですね」「pool_config
やresolve_pool_config
でrole
やshard
も指定できるようになった: シャーディングの条件によってコネクションプールを分けるなど、コネクションプールでこれらを細かく指定できると便利なときがありそうかなと思いました」
# activerecord/lib/active_record/connection_adapters/abstract/connection_handler.rb#L129
- pool_config = resolve_pool_config(config, owner_name)
+ pool_config = resolve_pool_config(config, owner_name, role, shard)
🔗 X-XSS-Protection
ヘッダーのデフォルト値が0
に変更
この
X-XSS-Protection
ヘッダーは既に非推奨化されており、このヘッダーを当初から実装していた主要なモダンブラウザ(ただしFirefoxでは実装されなかった)では、これをトリガーしていたXSS Auditorが既に削除されてContent Security Policy(CSP)に置き換わっている。OWASPでは、セキュリティ問題を増やす可能性のあるこのヘッダーを
0
に設定して古いブラウザのデフォルトの振る舞いを無効にするよう指示している。
この新しい振る舞いはRails 7.0からフレームワークのデフォルトとして追加された。
このヘッダーを0
に設定する理由については以下を参照:
つっつきボイス:「OWASPが正式にX-XSS-Protection
ヘッダーを0
にすべきと言っているならそのとおりにするのがよさそう👍」「X-XSS-Protection
ヘッダー自体を削除しないのはなぜなんだろう?🤔」「このヘッダーに対応しているブラウザがまだあるからじゃないかな: 一度広まったものをなくすのは大変」「ほんとにそうですよね」
参考: 9 デフォルトのヘッダー -- Rails セキュリティガイド - Railsガイド
後で調べると、現時点のsecure_headers gemはデフォルトのX-XSS-Protection
ヘッダーがまだ0
になっていないそうです↓。
🔗 PerThreadRegistry
を削除して非推奨化
つっつきボイス:「Rails内部用のPerThreadRegistry
はずっと前に非推奨化されていたけどwarningが出されていなかったので、あらためてwarningを出すようにしたそうです」「内部ということはRailsアプリ開発者が意識しなくていいヤツですね、よかった〜」
🔗Rails
🔗 Railsのsanitize_sql_like
は重要
つっつきボイス:「ちょうど今日のBPS社内勉強会でセキュリティを取り上げたときに話題になったRailsのsanitize_sql_like
メソッドを取り上げてみました」「そうそう、これはRails開発者がぜひとも知っておくべきサニタイズ用メソッドですね」
参考: RailsにてSQLでのワイルドカード文字をエスケープしてくれるsanitize_sql_likeは何をしているのか - Qiita
「これって何ですか?」「SQL文のLIKEの文字列でだけ機能する特殊文字は、通常のsanitize_sql
だけではエスケープされないんですよ」「え〜!そうだったんですか?」
「LIKEでは%
がワイルドカード文字というのはよく知られていますけど、実はアンダースコア_
も任意の1文字というワイルドカードなんですよ」「ありゃ〜、これは知っておかないとマズいヤツだ」「LIKEで使う文字列にsanitize_sql_like
をかけないとバグになりますし、下手をすると情報漏えいにもつながるので要注意」
# 安全な書き方(to_sしないと配列などをparamsに渡せてしまう)
hoge.where('name = ?', params[:name].to_s)
# =ではなくLIKEだと同じ書き方が危険になる(name値に%や_があるとワイルドカードと解釈される可能性)
hoge.where('name LIKE ?', params[:name].to_s + "%")
# LIKEの場合はsanitize_sql_likeを明示的にかける必要がある
hoge.where('name LIKE ?', ActiveRecord::Base.sanitize_sql_like(params[:name].to_s) + "%")
なお実際に使うときは+ "%"
よりも以下のように式展開"#{}"
にするのが一般的です。
hoge.where('name LIKE ?', "%#{ActiveRecord::Base.sanitize_sql_like(params[:name].to_s)}%")
訂正(2022/10/24)
jnchitoさんからご指摘をいただき、上のコードを修正・追記しました。ありがとうございます!🙇
どうもこんにちは。この記事にある以下のコード例ですが、ワイルドカード文字がどこにもないのでLIKE検索する必要のないコード例になってませんか?(フィヨルドブートキャンプ生がこれを見て悩んでました)
hoge.where('name LIKE ?', ActiveRecord::Base.sanitize_sql_like(params[:name].to_s))
— Junichi Ito (伊藤淳一) (@jnchito) October 23, 2022
「意外ですが、edgeガイドにもまだsanitize_sql_like
は載っていませんでした↓」「ちなみに自分はきっとそういうメソッドがあるはずだと思って探して見つけました」「たしかに、ないとおかしい機能ですよね」「今のコードでLIKEを使っているところを全文検索して探して修正してもいいぐらい大事」「この後やらなきゃ」
参考: Securing Rails Applications — Ruby on Rails Guides
🔗 AWS Lambda FunctionsをRubyで書く(Ruby Weeklyより)
つっつきボイス:「AWS Lambda FunctionsをRubyで書く手順を解説した記事、よさそう👍」
参考: AWS Lambda(イベント発生時にコードを実行)| AWS
「Lambda Functionsのコードを書くのは別に大変ではありませんが、そのコードをどう管理してデプロイするかの方に力を使いますよね」「そうそう」「管理やデプロイの方法はたくさんあるので、要件や用途に合わせて選ぶ方が大変: Lambda Functionsは単体で使うよりも他のコードと連携させるものなので、そうなるとIAMポリシーなどを適切に設定しないと動かないし、IAMポリシーも管理しようとするとTerraformを使うことになったりするのが悩ましい」
「サーバーレスフレームワークとTerraformをどう住み分けさせるかとかも考えないといけないですよね」「そうそう、サーバーレスフレームワーク単体でもできるけど、その場合は既存のTerraformとの整合性を壊さないようにしないといけないとかで複雑になりがち」
「AWS Cloud Development Kit(CDK)↓も使ってみたいんですが、Terraformと別管理になるとしたら複雑になるんじゃないかという点が気になって、まだ手出ししてません」「以前そのあたりを調べたことがあったんですが、割と強い権限が必要になる機能がいくつもあって、そのときは諦めました」「IAMのロールやポリシーを作成できる権限はどうしても強くなるので、それならTerraformでやる方がいいのかなという気持ちにもなります」「悩みは尽きない...」
参考: AWS クラウド開発キット – アマゾン ウェブ サービス
🔗 kredis: RedisをRuby風にアクセス可能にするラッパー(Ruby Weeklyより)
つっつきボイス:「Keyed Redisだからkredisなのか」「よく見たらGitHubのRailsリポジトリに置かれてるからRailsの機能なのかな?」「新しい割に★が多いですね」「DHHもコントリビュータに入っています」
「Kredis.string "mystring"
のように型名とキーを指定すると.value
で値を取り出せる、まさにキーバリューストアで型やJSONのようなデータ構造が使えるもののようですね↓」「GET
やSET
はRedisの操作なのか」
# 同リポジトリより
string = Kredis.string "mystring"
string.value = "hello world!" # => SET mystring "hello world"
"hello world!" == string.value # => GET mystring
integer = Kredis.integer "myinteger"
integer.value = 5 # => SET myinteger "5"
5 == integer.value # => GET myinteger
decimal = Kredis.decimal "mydecimal" # accuracy!
decimal.value = "%.47f" % (1.0/10) # => SET mydecimal "0.10000000000000000555111512312578270211815834045"
BigDecimal("0.10000000000000000555111512312578270211815834045e0") == decimal.value # => GET mydecimal
float = Kredis.float "myfloat" # speed!
float.value = 1.0/10 # => SET myfloat "0.1"
0.1 == float.value # => GET myfloat
boolean = Kredis.boolean "myboolean"
boolean.value = true # => SET myboolean "t"
true == boolean.value # => GET myboolean
datetime = Kredis.datetime "mydatetime"
memoized_midnight = Time.zone.now.midnight
datetime.value = memoized_midnight # SET mydatetime "2021-07-27T00:00:00.000000000Z"
memoized_midnight == datetime.value # => GET mydatetime
json = Kredis.json "myjson"
json.value = { "one" => 1, "two" => "2" } # => SET myjson "{\"one\":1,\"two\":\"2\"}"
{ "one" => 1, "two" => "2" } == json.value # => GET myjson
「見た感じではRedisの機能にひととおり対応していそう」「Kredis.list
という書き方もできるのね↓」
# 同リポジトリより
list = Kredis.list "mylist"
list << "hello world!" # => RPUSH mylist "hello world!"
[ "hello world!" ] == list.elements # => LRANGE mylist 0, -1
integer_list = Kredis.list "myintegerlist", typed: :integer
integer_list.append([ 1, 2, 3 ]) # => RPUSH myintegerlist "1" "2" "3"
integer_list << 4 # => RPUSH myintegerlist "4"
[ 1, 2, 3, 4 ] == integer_list.elements # => LRANGE myintegerlist 0 -1
「Active Recordモデルにもこうやって書けるのね↓」「RPUSH people:5:names "David" "Heinemeier" "Hansson"
みたいにActiveRecordのモデルに対してattributes的にRedisに格納する値を設定できる、へ〜!」「面白そう!」
# 同リポジトリ
class Person < ApplicationRecord
kredis_list :names
kredis_list :names_with_custom_key, key: ->(p) { "person:#{p.id}:names_customized" }
kredis_unique_list :skills, limit: 2
kredis_enum :morning, values: %w[ bright blue black ], default: "bright"
end
person = Person.find(5)
person.names.append "David", "Heinemeier", "Hansson" # => RPUSH people:5:names "David" "Heinemeier" "Hansson"
true == person.morning.bright? # => GET people:5:morning
person.morning.value = "blue" # => SET people:5:morning
true == person.morning.blue? # => GET people:5:morning
「RedisのプリミティブなAPIをRubyのシンプルなオブジェクトでラップしたRubyインターフェイスライブラリという感じかな」「RDBがあるのにkredisを使う理由って何だろう?🤔」「速度的な理由でRDBの代わりにRedisにデータを入れておきたいことはあるかも」「gem 'kredis'
をGemfileに追加してインストールとあるので、デフォルトでRailsに入るわけではなさそう」
前編は以上です。
バックナンバー(2021年度第4四半期)
週刊Railsウォッチ: フォームヘルパーの改修、Railsの監査ログgem比較、DHHとimport-mapほか(20211129前編)
- 20211116後編 Ruby Struct入門、書籍『進化的アーキテクチャ』、AWS Web問題集ほか
- 20211115前編 Rails 7がRuby 3.1のClass#descendantsに対応、GitHub Issue風ファイルアップローダほか
- 20211110後編 JSON.parseの機能、Opal 1.3、async gem、Linuxコマンドチートシートほか
- 20211102後編 2021年度Rubyアソシエーション開発助成、Rails REST APIレベルで楽観的ロックほか
- 20211101前編 Rails 7アセットパイプライン解説記事、ロジックをapp/operatorsで整理ほか
- 20211026後編 YJITがRuby 3.1向けにマージ、ripperのドキュメント化、crontabの罠ほか
- 20211025前編 insert_allやupsert_allのタイムスタンプ自動更新、rails/contextsにロジックを置くほか
- 20211019後編 ruby/debugをChromeでリモートデバッグ、Rubyアプリの最適化ほか
- 20211018前編 Railsリポジトリで進行中のPropshaft、inverse_ofを自動推論ほか
- 20211012後編 Ruby 3.1にYJITマージのプロポーザル、Rubyのmagic historyメソッド、JSのPartytownほか
- 20211011前編 ServerTimingミドルウェア追加、paramsで数値キーを許可、Railsで多要素認証ほか
- 20211006後編 ruby/debug 1.2.0リリース、Railsにはthorが入っている、tendejitほか
- 20211004前編 Rails 7でbyebugがruby/debugに変更、GitHub Codespacesをサポートほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)