Railsでのセキュリティリスクとその対策

代表的なセキュリティ攻撃とRailsでの対策を復習

CRSF(クロスサイトリクエストフォージェリ)

ログインした状態(セッションIDを維持した状態)を悪用してリクエストすること
ログイン後の利用者のみが利用可能なサービスを悪用したり(不正送金や商品購入、退会)、編集可能な情報を改竄できてしまう。
ログインした状態で不正なリンクをクリック。Webサイトを移り(クロス)、ユーザーの要求を偽って(フォージェリ)、悪意のあるリクエストが送信される

対策

  • GET/POSTを正しく使うこと
    GETは情報読み出し、POSTは状態変化や何らかの作用を伴う操作

  • GET以外のリクエストにはセキュリティトークンを使う
    リクエストがユーザーの意図にあるものかを確認するために、同じWebアプリから生じたリクエストであることを証明するためのセキュリティトークンを発行、照合する
    config.action_controller.default_protect_from_forgeryをtrueに設定すると自動的に実行(Rails5.2からデフォルトの設定)
    form_withなどを使ったリクエストは自動でセキュリティトークン(authenticity_token)が埋め込まれる

Ajaxリクエストへのセキュリティトークンの埋め込み
Ajaxリクエストでセキュリティトークンを送るには、JavaScriptが動く画面内にあらかじめセキュリティトークンを出力しておき、そのトークンを使ってX-CSRF-TokenというHTTPヘッダーで送る必要がある
出力はアプリのviewにある<%= csrf_meta_tags %>
fetch APIAjaxリクエストを送信する場合は自分でトークンを埋め込む必要がある

fetch('/posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json; charset=utf-8',
    'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
  }
})

XSS脆弱性があると、あらゆるCSRF保護が迂回されるので注意

XSS(クロスサイトスクリプティング)

テキストフォームなどの出力処理に問題がある場合に、スクリプトなどを埋め込まれてしまう
フィッシング詐欺(別サイトへ誘導)、セッションハイジャック(cookieを取得しなりすましができる)

対策

  • RailsXSS対策が標準で組み込まれていて、view処理を実行した際にHTMLタグに対してエスケープ処理が行われる。
    &,",<,>は、&amp;,&quot;,&lt;,&gtにそれぞれ置き換えられる
  • HTMLのエスケープ回避したいときのメソッド(html_safe/raw(<%==))を使う場合は、脆弱性が発生することになるので注意。html_escapeを使ってエスケープ。
  • ある程度タグをそのまま出したいが、危険なタグを出力したくない場合はSanitize Helperホワイトリスト方式で制限。

SQLインジェクション

パラメーター操作でDBをさわれてしまう
RailではSQL文字をフィルタする仕組みがあり', ", NULL, 改行をエスケープされるので、滅多に起こらなくなった。 Model.find(id)Model.find_by()といったクエリでは自動的にこの対策が適用されるが、
where("...")connection.execute()Model.find_by_sql()については手動でエスケープする必要あり

対策

# クエリメソッドに対してハッシュで条件を指定
users = User.where(name: params[:name])
# プレースホルダを利用
users = User.where('name = ?', params[:name])

LIKEは演算子は自動的にサニタイズされないので注意。sanitize_sql_likeを使う必要がある

参考

学んだことがあれば都度追加していく。

Spotify APIを利用してプレイリストを作った

プログラミングを使って遊んでみた記事です。

最近札幌から東京に越してきたものの、札幌のラジオが恋しい…
NORTH WAVEというFM局をよく聴いていて、選曲がトレンドを押さえている&マイナーなアーティストもカバーしてくれているので、音楽をキャッチするのにいつもお世話になっていた。
同じJFL系のJ-WAVEもあるんだけど、似て非なる感じで。
radikoのプレミアム会員になれば解決なんだけど、そこまでの熱はないな…

そこで考えたのが、週に1回、北海道でよく聞かれた曲を集計し、ランキング化しているSAPPORO HOT 100の曲を詰め込んだプレイリストをつくること
このプレイリストを聴いておけばトレンドに触れられそう。
あとSpotifyは元々好きなサービスで、APIを利用してみたかったというのもある。   

早速プログラムしてみる。

当初のイメージ

https://www.fmnorth.co.jp/hot100/からアーティストと曲を取得し保存
→ Spotify APIでプレイリストを作成し取得した曲を追加
→ Github Actionsで毎週定期実行

こんなプログラムを

require 'nokogiri'
require 'open-uri'
require 'dotenv/load'
require 'rspotify'

# サイトから曲とアーティストを取得
url = 'https://www.fmnorth.co.jp/hot100/'
doc = Nokogiri::HTML(URI.open(url))

playlists = doc.css('.hot100List__detail').map do |row|
  track = row.at_css('.musicTitle').text.strip
  artist = row.at('.hot100List__detail--artist').text.strip
  { title: track, artist: artist }
end

# 承認リクエスト
client_id = ENV['CLIENT_ID']
client_secret = ENV['CLIENT_SECRET']
RSpotify.authenticate(client_id, client_secret)

# Spotifyのプレイリストを探し、先週分の全曲を削除
spotify_playlist = RSpotify::Playlist.find_by_id(ENV['PLAYLIST_ID'])

track_numbers = (0..spotify_playlist.total - 1).to_a
spotify_playlist.remove_tracks!(track_numbers, snapshot_id: spotify_playlist.snapshot_id)

# Spotifyのプレイリストに今週分の曲を追加
tracks = playlists.flat_map do |list|
  RSpotify::Track.search("#{list.artist} #{list.track}", limit: 1, market: 'JP')
end
spotify_playlist.add_tracks!(tracks)

Spotify APIRubyではrspotifyというRubyのラッパーがあり便利。
クライアントIDとクライアントシークレットを使って認証、プレイリスト内の曲をカスタマイズするといった流れ。

これを定期実行すればプレイリストができる!と思ったところでプレイリスト内の曲を削除や追加ができずに以下のエラー
uninitialized class variable @@users_credentials in RSpotify::User (NameError)

Spotify APIの認証について調べてみると、認証フローは4つ。

Authorization | Spotify for Developers

自分のはClient credentialsフローで、これだけだとユーザー認証が行われずプレイリストを触れないとのこと


ということでAuthorization Codeフロー(認可コードフロー)で再実装。

どうしても「サービスと連携しますか」みたいな画面で「はい」とする認証ダイアログが必要。
何やら自動化は難しそうだなと思い、慣れているRails×Omniauth×rspotifyという形で実装する。
viewに簡単なボタンを1つつけて、それを押すことで、認証→音楽を取得しDB保存→プレイリストに曲を追加とする形にした。

自動化はできなかったけど、ローカルでサーバー立ち上げて、ボタン1つでプレイリストが作れるんだから週に1回押すか….
と思ったけど、よくよく認可コードフローのページを読み直したり、以下のサイトを参考にすると、1度認証してしまえばrefreshed access_tokenを使って自動化できそう。

qiita.com

今回は時間切れということで、いつかまた続きをやってみたい。
Spotify APIを触るのは楽しかった。
認証関連の知識がまだ曖昧だと@yocajiiさんとのペアプロ時に話したら、この本を紹介してもらったので、勉強してから再実装としたい。

booth.pm

そして以前に、@jnchitoさんの下の記事を読んだ時から、日常の困りごと何か解決したいなと思ってたので、その第一歩になったかな?

blog.jnito.com


作ったプレイリストはこちら

いつでも札幌のトレンドが聞けるようになった。
結局radikoのプレミアム会員になるほうが早かったかも。

mysql2インストール時のエラー対応

AWSについて学習中。Railsアプリをあげようと思ったらエラーが出てややハマったのと、同様のエラー記事がなかったので残しておく。

Apple M1
ruby 3.1.3
Rails 7.0.4.1

gemからmysql2をインストールしようとすると以下のようなエラーが出た。

$gem install mysql2
Building native extensions. This could take a while...
ERROR:  Error installing mysql2:
        ERROR: Failed to build gem native extension.

    current directory: /Users/username/.rbenv/versions/3.1.3/lib/ruby/gems/3.1.0/gems/mysql2-0.5.5/ext/mysql2
/Users/username/.rbenv/versions/3.1.3/bin/ruby -I /Users/username/.rbenv/versions/3.1.3/lib/ruby/3.1.0 extconf.rb
checking for rb_absint_size()... yes
checking for rb_absint_singlebit_p()... yes
checking for rb_gc_mark_movable()... yes
checking for rb_wait_for_single_fd()... yes
checking for rb_enc_interned_str() in ruby.h... yes
-----
Cannot find library dir(s) /usr/local/opt/openssl@1.1/lib
-----
*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
        --with-opt-dir
        --without-opt-dir
        --with-opt-include
        --without-opt-include=${opt-dir}/include
        --with-opt-lib
        --without-opt-lib=${opt-dir}/lib
        --with-make-prog
        --without-make-prog
        --srcdir=.
        --curdir
        --ruby=/Users/username/.rbenv/versions/3.1.3/bin/$(RUBY_BASE_NAME)
        --with-openssl-dir
        --with-openssl-dir
        --with-openssl-include
        --without-openssl-include=${openssl-dir}/include
        --with-openssl-lib
        --without-openssl-lib=${openssl-dir}/lib

To see why this extension failed to compile, please check the mkmf.log which can be found here:

  /Users/username/.rbenv/versions/3.1.3/lib/ruby/gems/3.1.0/extensions/arm64-darwin-22/3.1.0/mysql2-0.5.5/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in /Users/username/.rbenv/versions/3.1.3/lib/ruby/gems/3.1.0/gems/mysql2-0.5.5 for inspection.
Results logged to /Users/username/.rbenv/versions/3.1.3/lib/ruby/gems/3.1.0/extensions/arm64-darwin-22/3.1.0/mysql2-0.5.5/gem_make.out

openssl関連で、--with-ldflags--with-cppflagsオプションをつければインストールできるという記事が多いのでやってみるが同じエラーが続く。

エラー文読み直しにて、Cannot find library dir(s) /usr/local/opt/openssl@1.1/libが怪しい。

そもそも/usr/local/opt/openssl@1.1/libというディレクトリがなく、そりゃ見つからないと怒られてしまう。

なんでこんなことになっているのかと思ったら.zshrc

export RUBY_CONFIGURE_OPTS="--with-openssl-dir=/usr/local/opt/openssl@1.1"

という環境変数が設定されていた。多分、学習初期に訳もわからず設定したものな気がする。

これを存在するパスに。Home · rbenv/ruby-build WikiをみるとRuby 3.1以降ではOpenSSL3が必要とのことで

export RUBY_CONFIGURE_OPTS="--with-openssl-dir=$(brew --prefix openssl@3)"

こうすることで無事にmysql2をインストールできた。

AtCoderで使ってるreturnって何?

AtCoderにてループを抜けるのにreturnをよく使っている。
配列をループさせ該当するものがあれば何か出力して終了、該当しない場合は別の文字を返すというような問題。

sum = 0
numbers.each do |n|
  sum += n
  return puts '10を超えたよ' if sum > 10
end
puts '10を超えなかったよ'

配列numbersの要素を次々にsumに加えていって、sumが10超えたところで「10を超えたよ」、最終的に超えなければ「10を超えなかったよ」と出力される。

先日@haruguchiさん主催のモブプロ会にて、いつも通りreturnを使ったんだが、このreturnってどうなってるんだっけと言語化できなかったのでメモ。

結論:トップレベルでのreturnなので、出力後にそのままプログラムが終了した

array.each do |a|
  # トップレベルでの return なのでループを抜けるのではなくプログラムを抜ける
  return puts 'foo' if condition
end

上のコードの様々な箇所でp selfとすると、全てmainが返る。mainというのはトップレベルでselfを表すもの。
今回は何もメソッドやクラス、モジュールを作成していないので、eachの中でだろうが外だろうが、そこはトップレベルにある。
returnはメソッドからの脱出という認識でいたが、

トップレベルで return した場合はプログラムが終了します。

制御構造 (Ruby 3.2 リファレンスマニュアル)にそのまま書いてあった。
(といっても実務でのreturnは早期リターン(ガード節)で使い、このような使われ方はなさそうな気が。)

プログラム全体を終了させるKernel.#exit (Ruby 2.7.0 リファレンスマニュアル)と同じことができる。

sum = 0
numbers.each do |n|
  sum += n
  if sum > 10
    puts '10を超えたよ'
    exit
  end
end
puts '10を超えなかったよ'

ちなみにp self.classmainのクラスを見るとObject。Objectクラスのインスタンスmain
ObjectにはKernelモジュールがincludeされているので、トップレベルでpputsが使える。
チェリー本にあった内容、なんだか点と点が繋がってきた感覚。

参考

AtCoderのB問題が解けるようになった

この記事はフィヨルドブートキャンプ Part 1 Advent Calendar 2022 - Adventarの13日目の記事です。
昨日はHikaruさんの「オブジェクト指向設計実践ガイド(POODR) 感想 & 第1章 まとめ - Light! More Light!」でした。

フィヨルドブートキャンプの今年のアドベントカレンダーはこちらです。

はじめに

こんにちは。フィヨルドブートキャンプ(以下、FBC)卒業生のガラムマサラです。
1日の学習初めの小手試しや、少しだけ時間があって何かコードを書きたいというときに、よくAtCoderの過去問を解いています。
これまでのAtCoderの取り組みや、B問題を解くためのTipsを本記事では紹介してみます。

AtCoderについて

AtCoderはプログラミングを使って問題を解くことを競技化したコンテストを開催するサイト・運営会社です。
AtCoder Beginner Contest、通称ABCは定期的に開催している初心者向けコンテストで、毎週A〜H問題が出題されています。
難易度としてA問題は「プログラミング言語の文法を確認する程度」、B問題は「FizzBuzzを応用する程度」とされています。
過去問が掲載されているので、コンテスト期間とは関係なく問題を解くことができ、各問題に解説があったり、他人の解答を見ることができるので、大変勉強になります。
AtCoderを使った勉強法に関しては、同じAtCoder学習仲間であるsugieさんが一昨日書かれた記事が参考になります。

sugie.co

わたしとAtCoder

FBCRubyのプラクティスが終わった頃、同期のあんすとさんの紹介でAtCoderの過去問を解き始めました。
最初は標準入力ってどうやって受け取るの?という状態でしたが、それに慣れると少しずつA問題が解くことができました。
B問題となるとループを含んだ実装になるので、eachを使うのでやっとだった自分はほとんど手がつけられない状況でした。

FBCで学び、チーム開発や自作サービスでRubyのコードを書いていった中、久々にB問題に取り組んでみると、「あれ、なんだか解けるぞ!」とブレイクスルーを感じられて嬉しかったです。
多くのRubyistが言う「Rubyで書くが楽しい」ということも、なんとなく感じ取れている気がします。
本日までにB問題は81問解いていました。新しめの問題の方が難しいので、できるだけそちらを解くようにしています。

以降はそのB問題攻略の解き方のコツや便利なRubyメソッドを紹介、一緒に実際の問題も載せておきます。
AtCoder初心者の方の参考になるかもしれません(今回は解くことが目的で、計算量については考えられていないのであしからず…)

標準入力から受け取る

整数nを1つ受け取とった後、n行のスペース区切りの整数を二次元配列で受け取る方法です。

2
1 3 5
2 4 6
n = gets.to_i #=> 2
array = n.times.map { gets.split.map(&:to_i) }
#=> [[1, 3, 5], [2, 4, 6]]
array = n.times.map do
  gets.split(' ').map do |line|
    line.to_i
  end
end
#=> [[1, 3, 5], [2, 4, 6]]

頻繁に出題される形です。
Array.newreadlinesを使うなど様々な方法がありますが、1つ得意な形を持っておくと応用も効きやすいので良いかなと思います。

全探索

考えられる全ての場合を調べることです。
表や二次元盤面を要する問題など、よくこの方法を使います。
主に二重、場合によって三重でeachtimesを使って繰り返すことで全探索できることが多いです。

array = [1, 2, 3, 4, 5, 6, 7, 8, 9]
count = 0
array.each do |i|
  array.each do |j|
    count += 1 if (i * j).even?
  end
end
puts count
#=> 56

九九の表を1つ1つ評価しているイメージです。

全探索で解いた問題


全探索をするよりArrayやEnumerableモジュールのメソッドで簡単に解決できるものもあります。

Enumerable#each_with_index

要素の添え字付きの繰り返し処理です。

array = ['a', 'b', 'c']
array.each_with_index do |a, i|
  p "#{i}: #{a}"
end
#=> "0: a"
#   "1: b"
#   "2: c"

添え字を0以外からにしたいときにはEnumerator#with_indexを使います。

array.each.with_index(1) do |a, i|
  p "#{i}: #{a}"
end
#=> "1: a"
#   "2: b"
#   "3: c"

each_with_indexで解いた問題

Array#combinationArray#permutation

combinationは並び順を考慮しない組み合わせを、permutationは並び順を考慮する順列を生成するメソッド。
引数で選び出す要素の個数を指定できます。

array = [1, 2, 3, 4]
array.combination(2).to_a
#=> [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
array.permutation(2).to_a
#=> [[1,2],[1,3],[1,4],[2,1],[2,3],[2,4],[3,1],[3,2],[3,4],[4,1],[4,2],[4,3]]

重複を許可するArray#repeated_combinationArray#repeated_permutationもあるので、このあたりは公式リファレンスを確認します(まだ使ったことがない気がする)

combinationで解いた問題

Enumerable#each_cons

要素を重複ありで引数nずつに区切り、ブロックに渡して繰り返すメソッド。
1回のループで要素を1つずつしか取得できないeachに比べ、こちらは複数個の要素を扱えるので便利です。

array = [1, 2, 3, 4]
array.each_cons(2){ |v| p v }
#=> [1, 2]
#   [2, 3]
#   [3, 4]

each_consで解いた問題

Array#transpose

行と列の入れ換えをするメソッド。
個人的にFBCのプラクティス「ls コマンドを作る」で使ったメソッドなので思い出に残っています。

[[1, 2],[3, 4],[5, 6]].transpose
#=> [[1, 3, 5], [2, 4, 6]]

transposeで解いた問題

Array#find_index

indexとしても同様です。配列内の指定した要素の最初の位置を取得するメソッド。
配列内に重複している要素があり、その一つだけを取り出したい場合に、「最初の」インデックス値を取得し操作できるので便利です。
Array#rindexは最後から順に探してくれます。

# 2をひとつだけ削除したい
array = [1, 2, 2, 3, 3, 3]
# これでは2を全て削除してしまう
array.delete(2)
array #=> [1, 3, 3, 3]

# インデックス値を取得できるsと1つだけ削除可
array.delete_at(array.index(2))
array #=> [1, 2, 3, 3, 3]

indexで解いた問題

Enumerable#tally

要素を数え上げ、Hashで返すメソッド。
このメソッドは、僕のAtCoderの先生でもあるharuguchiさんから教えてもらいました。
tallyを使って簡潔に解ける問題も多く、個人的に一番好きなメソッドになりました。

['a', 'b', 'b', 'c', 'c', 'c'].tally
#=> {"a"=>1, "b"=>2, "c"=>3}

tallyで解いた問題


配列の合計

配列の和はArray#sum、要素を全て掛けるにはEnumerable#injectを使っています

array = [1, 2, 3, 4]
array.sum #=> 10
array.inject(:*) #=> 24

配列同士の積を求める問題

四捨五入・切り上げ・切り捨て

A問題から数字の四捨五入や切り上げ・切り捨てが出てくる頻度は高いです。
切り上げ切り捨てがどっちかよく混乱していたんですが、これもharuguchiさんから教えてもらったシーリングライト(天井)とフロア(床)のイメージで定着しました。

1.4.round #=> 1
1.5.round #=> 2
1.1.ceil #=> 2
1.9.floor #=> 1

四捨五入・切り上げ・切り捨てに関する問題

参考文献

プロを目指す人のためのRuby入門[改訂2版

FBCメンターの@jnchitoさんが執筆された、言わずと知れたチェリー本です。
配列、Range、Hash、繰り返し処理で迷ったときは必ずチェリー本に戻りました。答えはここに全部詰まっています!

プログラミングコンテストAtCoder入門

こちらは本記事を書くにあたって読んでみました。 Rubyではなく、C++とPython3のコードを用いて解説されているので、ソースコードに沿っての学習はできませんが、アルゴリズムを知る上で多くの知見が得られます。
この本によると自分は初級編をクリアしたところでした。中級編に進んでいきます!

さいごに

AtCoder運営の方々、いつも楽しませていただいています。ありがとうございます。 今後は実際に時間を調整してコンテストに参加してみたり、アルゴリズムの知識や計算量の意識が必要になってくるC問題以降に手をつけていきたいと考えています。

FBCでは、だいたい木曜日の昼頃にharuguchiさん主催でAtCoderを解く「勝手にモブプロ会」が行われています。
いつも楽しく学びのある会です。haruguchiさん、ありがとうございます!
FBCAtCoderが気になっている方がいましたら、一緒にモブプロできると嬉しいです。

明日のFBCアドベントカレンダーは、Part1をkazumiさん、Part2をumizaruさんが記事を書いてくださいます。楽しみです!

Rails以外のgemをバージョンアップして、Dependabotを入れた

現在、フィヨルドブートキャンブ内にて、@Sakiさん主催の「パーフェクト Ruby on Rails輪読会」が行われています。 こちらの本、大変参考になりますが、なんだか難しいし、もう少し深掘りがしたい!と思いまして、このブログに予習・復習した内容を簡単に載せていきたいなと考えています。

9-2-1

ライブラリは新機能追加、バグ修正など日々更新されていくので、アプリで使っているgemの定期的なアップデートが必要。
Dependabotを導入しておくことで、bundle updateされたgemのPRが自動生成される。
ということで、自作アプリのNursePicksに導入してみる。

Railsでは、導入前に手動でbundle updateすることを推奨しているので、フィヨルドブートキャンプのメンターでもある伊藤さんのありがたい記事を参考に、ひとまずgemのバージョンアップをしていく。


はじめに、テストが通るか否かと、Simplecovでカバレッジも確認。
94%とそこそこの水準で書き漏れがないことも確認。

それではgemのバージョンアップ。
bundle outdatedというコマンドで、現バージョンと最新バージョンの差分を確認できる。

Gem                     Current         Latest          Requested  Groups
actioncable             6.1.6           7.0.4
actionmailbox           6.1.6           7.0.4
actionmailer            6.1.6           7.0.4
actionpack              6.1.6           7.0.4
actiontext              6.1.6           7.0.4
actionview              6.1.6           7.0.4
activejob               6.1.6           7.0.4
activemodel             6.1.6           7.0.4
activerecord            6.1.6           7.0.4
activestorage           6.1.6           7.0.4
activesupport           6.1.6           7.0.4
addressable             2.8.0           2.8.1
bootsnap                1.11.1          1.15.0          >= 1.4.4   default
brakeman                5.3.1           5.4.0           >= 0       development, test
buftok                  0.2.0           0.3.0
bullet                  7.0.2           7.0.4           >= 0       development
capybara                3.37.1 d08e88d  3.38.0 5c86747  >= 0       test
dotenv                  2.7.6           2.8.1
dotenv-rails            2.7.6           2.8.1           >= 0       default
erubi                   1.10.0          1.11.0
faraday                 1.10.0          2.7.1
faraday-http-cache      2.3.0           2.4.1
faraday-net_http        1.0.1           3.0.2
faraday-retry           1.0.3           2.0.0
http                    4.4.1           5.1.0
http_parser.rb          0.6.0           0.8.0
i18n                    1.10.0          1.12.0
jwt                     2.3.0           2.5.0
meta-tags               2.16.0          2.18.0          >= 0       default
metainspector           5.12.1          5.13.1          >= 0       default
minitest                5.15.0          5.16.3
msgpack                 1.5.1           1.6.0
multipart-post          2.1.1           2.2.3
oauth                   0.5.10          1.1.0
oauth2                  1.4.9           2.0.9
omniauth-google-oauth2  1.0.1           1.1.1           >= 0       default
omniauth-oauth2         1.7.2           1.8.0
parser                  3.1.2.0         3.1.3.0
pg                      1.3.5           1.4.5           ~> 1.1     default
public_suffix           4.0.7           5.0.0
puma                    5.6.4           6.0.0           ~> 5.0     default
rack                    2.2.3           3.0.1
rack-mini-profiler      2.3.4           3.0.0           ~> 2.0     development
rack-protection         2.2.0           3.0.4
rack-proxy              0.7.2           0.7.4
rack-test               1.1.0           2.0.2
rails                   6.1.6           7.0.4           ~> 6.1.6   default
railties                6.1.6           7.0.4
rb-fsevent              0.11.1          0.11.2
regexp_parser           2.4.0           2.6.1
rspec-core              3.11.0          3.12.0
rspec-expectations      3.11.0          3.12.0
rspec-mocks             3.11.1          3.12.0
rspec-rails             5.1.2           6.0.1           >= 0       development, test
rspec-support           3.11.0          3.12.0
rubocop                 1.29.1          1.39.0
rubocop-ast             1.18.0          1.23.0
rubocop-fjord           0.2.0           0.2.2           >= 0       development
rubocop-performance     1.14.0          1.15.1
rubocop-rails           2.14.2          2.17.3          >= 0       development
rubocop-rspec           2.11.1          2.15.0          >= 0       development
selenium-webdriver      4.5.0           4.6.1
spring                  4.0.0           4.1.0           >= 0       development
sprockets               4.0.3           4.1.1
temple                  0.8.2           0.9.1
tilt                    2.0.10          2.0.11
tzinfo                  2.0.4           2.0.5
unicode-display_width   2.1.0           2.3.0
zeitwerk                2.5.4           2.6.6

RailsはVue.jsの導入関連で6.1.6でnewしていたので、Rails関連を除くと大体がマイナーバージョンやリビジョンの更新になるっぽい。
半年前に作り始めた小規模アプリなので、bundle updateで一括バージョンアップしても良さそうだが、ここは基本に忠実に。

伊藤さんの記事には原則gemのバージョン指定はしないとあり。指定する場合はコメントを書いておくと良いとのこと。
不必要なバージョン指定を外す。

続いては、developmentとtestグループのgemを先にアップデート。
記事にある通りbundle update -g development -g testとコマンドでアップデートしてみたが、自分の環境ではtestグループのみアップデートになってしまったので、(development + testグループがあったことが起因してそうだけど、深掘りはしない)

bundle update -g development
bundle update -g test

としてアップデート。テストも問題なく通るし、ローカルでの挙動も問題なし。
再びbundle outdatedをしてみる。

Gem                     Current  Latest  Requested  Groups
actioncable             6.1.6    7.0.4
actionmailbox           6.1.6    7.0.4
actionmailer            6.1.6    7.0.4
actionpack              6.1.6    7.0.4
actiontext              6.1.6    7.0.4
actionview              6.1.6    7.0.4
activejob               6.1.6    7.0.4
activemodel             6.1.6    7.0.4
activerecord            6.1.6    7.0.4
activestorage           6.1.6    7.0.4
activesupport           6.1.6    7.0.4
bootsnap                1.11.1   1.15.0  >= 1.4.4   default
buftok                  0.2.0    0.3.0
dotenv                  2.7.6    2.8.1
dotenv-rails            2.7.6    2.8.1   >= 0       default
faraday                 1.10.0   2.7.1
faraday-http-cache      2.3.0    2.4.1
faraday-net_http        1.0.1    3.0.2
faraday-retry           1.0.3    2.0.0
http                    4.4.1    5.1.0
http_parser.rb          0.6.0    0.8.0
jwt                     2.3.0    2.5.0
meta-tags               2.16.0   2.18.0  >= 0       default
metainspector           5.12.1   5.13.1  >= 0       default
msgpack                 1.5.1    1.6.0
multipart-post          2.1.1    2.2.3
oauth                   0.5.10   1.1.0
oauth2                  1.4.9    2.0.9
omniauth-google-oauth2  1.0.1    1.1.1   >= 0       default
omniauth-oauth2         1.7.2    1.8.0
pg                      1.3.5    1.4.5   ~> 1.1     default
puma                    5.6.4    6.0.0   ~> 5.0     default
rack                    2.2.4    3.0.1
rack-protection         2.2.0    3.0.4
rack-proxy              0.7.2    0.7.4
rails                   6.1.6    7.0.4   ~> 6.1.6   default
railties                6.1.6    7.0.4
sprockets               4.0.3    4.1.1
temple                  0.8.2    0.9.1

気になるのはoauth2が1.4.9→2.0.9くらい。
一応CHANGELOGを確認。Rubyの対応バージョンを変えたとかでした。

ということでbundle updateを実行 テストも通り、bundle outdatedをしても、残りはRails関連のgemだけでスッキリ。

Gem             Current  Latest  Requested  Groups
actioncable     6.1.7    7.0.4
actionmailbox   6.1.7    7.0.4
actionmailer    6.1.7    7.0.4
actionpack      6.1.7    7.0.4
actiontext      6.1.7    7.0.4
actionview      6.1.7    7.0.4
activejob       6.1.7    7.0.4
activemodel     6.1.7    7.0.4
activerecord    6.1.7    7.0.4
activestorage   6.1.7    7.0.4
activesupport   6.1.7    7.0.4
buftok          0.2.0    0.3.0
http            4.4.1    5.1.0
http_parser.rb  0.6.0    0.8.0
puma            5.6.5    6.0.0   ~> 5.0     default
rack            2.2.4    3.0.1
rails           6.1.7    7.0.4   ~> 6.1.6   default
railties        6.1.7    7.0.4
temple          0.8.2    0.9.1

ここからはDependabotを導入していく。
Railsで学習した形でyamlファイルを作ろうと思ったが、オプションに変更があったりなどで、GitHubのDocsGitHub の Dependabot version updates で依存ライブラリを継続的に更新するという参考記事から学ぶ。
後者の記事はGithub上の動作を写真付きで載せてくれているので分かりやすかった。

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: 'bundler'
    directory: '/'
    schedule:
      interval: "weekly"
      day: "monday"
      time: "06:00"
      timezone: "Asia/Tokyo"

これにてDependabotの導入が完了。毎週月曜日に通知が来ることになった。

導入後は早速Railsのバージョンを上げてみようとの提案が…

次はRails7に上げてWebpackerを剥がせるとよさそう。

Brakemanを自作アプリに導入してみた

現在、フィヨルドブートキャンブ内にて、@Sakiさん主催の「パーフェクト Ruby on Rails輪読会」が行われています。 こちらの本、大変参考になりますが、なんだか難しいし、もう少し深掘りがしたい!と思いまして、このブログに予習・復習した内容を簡単に載せていきたいなと考えています。

9-3-2

Brakemanはアプリの脆弱性を静的解析してくれる便利なgem

RailsConfでの発表の動画を見る限り、読み方は「ブレーキマン」ではなく「ブレークマン」っぽい。
使い所はCIで定期実行して、脆弱性につながる疑いのコードを事前に検知するとよいとのこと。
XSSSQLインジェクションなどの脆弱性を確認してくれ、その確認項目は30項目くらいありそう。

Railsで作ったアプリでは警告等が出なかったので、せっかくなので自作アプリのNursePicksに導入してみる。

group :development do
  gem 'brakeman'
end

bundle exec brakemanで実行してみると、レポートが作成された。

== Brakeman Report ==

Application Path: work/nursepicks
Rails Version: 6.1.6
Brakeman Version: 5.3.1
Scan Date: 2022-11-16 17:06:57 +0900
Duration: 1.213567 seconds
Checks Run: BasicAuth, BasicAuthTimingAttack, CSRFTokenForgeryCVE, ContentTag, CookieSerialization, CreateWith, CrossSiteScripting, DefaultRoutes, Deserialize, DetailedExceptions, DigestDoS, DynamicFinders, EOLRails, EOLRuby, EscapeFunction, Evaluation, Execute, FileAccess, FileDisclosure, FilterSkipping, ForgerySetting, HeaderDoS, I18nXSS, JRubyXML, JSONEncoding, JSONEntityEscape, JSONParsing, LinkTo, LinkToHref, MailTo, MassAssignment, MimeTypeDoS, ModelAttrAccessible, ModelAttributes, ModelSerialize, NestedAttributes, NestedAttributesBypass, NumberToCurrency, PageCachingCVE, PermitAttributes, QuoteTableName, Redirect, RegexDoS, Render, RenderDoS, RenderInline, ResponseSplitting, RouteDoS, SQL, SQLCVEs, SSLVerify, SafeBufferManipulation, SanitizeConfigCve, SanitizeMethods, SelectTag, SelectVulnerability, Send, SendFile, SessionManipulation, SessionSettings, SimpleFormat, SingleQuotes, SkipBeforeFilter, SprocketsPathTraversal, StripTags, SymbolDoSCVE, TemplateInjection, TranslateBug, UnsafeReflection, UnsafeReflectionMethods, ValidationRegex, VerbConfusion, WithoutProtection, XMLDoS, YAMLParsing

== Overview ==

Controllers: 11
Models: 6
Templates: 17
Errors: 0
Security Warnings: 4

== Warning Types ==

Cross-Site Scripting: 4
Cross-Site Scripting: 4

== Warnings ==

Confidence: Weak
Category: Cross-Site Scripting
Check: SanitizeConfigCve
Message: rails-html-sanitizer 1.4.2 is vulnerable to cross-site scripting when `select` and `style` tags are allowed (CVE-
2022-32209). Upgrade to 1.4.3 or newer
File: Gemfile.lock
Line: 273

Confidence: Weak
Category: Cross-Site Scripting
Check: LinkToHref
Message: Potentially unsafe model attribute in `link_to` href
Code: link_to(Post.find(params[:post_id]).url, :target => :_blank, :rel => "noopener", :class => "post-image")
File: app/views/posts/show.html.slim
Line: 5

Confidence: Weak
Category: Cross-Site Scripting
Check: LinkToHref
Message: Potentially unsafe model attribute in `link_to` href
Code: link_to(Post.find(params[:post_id]).title, Post.find(params[:post_id]).url, :target => :_blank, :rel => "noopener")
File: app/views/posts/show.html.slim
Line: 9

Confidence: Weak
Category: Cross-Site Scripting
Check: LinkToHref
Message: Potentially unsafe model attribute in `link_to` href
Code: link_to(User.find(params[:id]).url, User.find(params[:id]).url)
File: app/views/users/show.html.slim
Line: 11

何やらCross-Site Scriptingで4件の警告あり。
内訳としては3件がLinkToHref、1件がSanitizeConfigCve
1つ1つの警告の詳細についてはbrakeman/docs/warning_types/

link_toメソッドの指摘に関しては

Brakemanはlink_toメソッドを使って作成されたURLにユーザー入力が含まれていると警告を発します。Railsにはこの方法で作成されたURLを安全にする方法がないため(例:プロトコルをHTTP(S)に限定する、など)、安全なメソッドを無視したい場合は次のオプションを使ってください。 Brakeman: Options

伊藤さんが翻訳してくれている。
ということで今回はurlメソッドで怒られてるので、--url-safe-methods urlとオプションをつけておく。

あとはrails-html-sanitizerをアップグレード。
対象コードを修正すると、

== Overview ==

Controllers: 11
Models: 6
Templates: 17
Errors: 0
Security Warnings: 0

== Warning Types ==


No warnings found

ということで修正完了。

GithubActionsにも設定、CIでも実行できるようにした。

- name: Breakeman
  run: bundle exec brakeman --url-safe-methods url

ちなみにbrakeman -Iコマンドで無視する警告を設定できるとのこと
参考記事:Brakemanで無視する警告の設定の仕方 - Qiita
breakman -Aでデフォルトでは検証しない、すべてのチェックも実行してみたが、Unscoped Findなど偽陽性のものも多いので、この設定をしておけばよさそう。

その他参考記事

セキュリティ対策ができるし、特に自作アプリでは変更のたびに誰かにレビューしてもらうことはないので、こういった静的解析ツールは便利!