Madogiwa Blog

主に技術系の学習メモに使っていきます。

lockファイル内のライブラリがどのライブラリによってinstallされているか調べる方法MEMO

間接的に依存しているライブラリに脆弱性がある場合等、自身のpackgae.jsonのどのpackageを更新すれば解消できるか等を調べたい場合、lockファイルを頑張って確認するのは大変です。

package manager(yarn, npm)の機能を使うと割と簡単に調べられたのでメモ🗒

yarn

yarnだとwhyという機能が提供されていています。

yarn why | Yarn

使い方は簡単でyarn why package-nameで実行すると以下のように依存関係を出力してくれます。

$ yarn why minimist
yarn why v1.22.17
[1/4] 🤔  Why do we have the module "minimist"...?
[2/4] 🚚  Initialising dependency graph...
[3/4] 🔍  Finding dependency...
[4/4] 🚡  Calculating file sizes...
=> Found "minimist@1.2.6"
info Reasons this module exists
   - "@lhci#cli#chrome-launcher#mkdirp" depends on it
   - Hoisted from "@lhci#cli#chrome-launcher#mkdirp#minimist"
   - Hoisted from "@lhci#cli#update-notifier#latest-version#package-json#registry-auth-token#rc#minimist"
info Disk size without dependencies: "104KB"
info Disk size with unique dependencies: "104KB"
info Disk size with transitive dependencies: "104KB"
info Number of shared dependencies: 0
✨  Done in 0.20s.

info Reasons this module existsで最上位に記載されているpackageが自身が直接依存している(package.jsonに記載している)packageということですね。便利!

npm

npmではnpm lsという機能が提供されています。

docs.npmjs.com

こちらも使い方は簡単でnpm ls package-nameで依存関係のツリーを表示してくれます。

$ npm ls kind-of
my_package@1.0.0 /foo/bar
└─┬ webpack-cli@4.4.0
  └─┬ webpack-merge@5.7.3
    └─┬ clone-deep@4.0.1
      ├── kind-of@6.0.3
      └─┬ shallow-clone@3.0.1
        └── kind-of@6.0.3 deduped

my_packageを除いた最上位に記載されているpackageが自身が直接依存している(package.jsonに記載している)packageということですね。便利!

おまけ: RubyGems

bundle exec gem dependency -Rで調べられるっぽい。リストが間接的に依存しているもので、Used byにinstall済みで直接 nokogiriに依存しているライブラリっぽい(?)、引数にはgem名を表す正規表現を指定する。完全一致にしたい場合には^foo$で完全一致に該当する正規表現を設定してあげれば良さそう。

bundle exec gem dependency -R "^nokogiri$"
Gem nokogiri-1.13.3-arm64-darwin
  bundler (~> 2.2, development)
  hoe-markdown (~> 1.4, development)
  minitest (~> 5.15, development)
  minitest-reporters (~> 1.4, development)
  racc (~> 1.4)
  rake (~> 13.0, development)
  rake-compiler (= 1.1.7, development)
  rake-compiler-dock (~> 1.2, development)
  rdoc (~> 6.3, development)
  rexical (~> 1.0.7, development)
  rubocop (~> 1.23, development)
  rubocop-minitest (~> 0.17, development)
  rubocop-performance (~> 1.12, development)
  rubocop-rake (~> 0.6, development)
  rubocop-shopify (~> 2.3, development)
  ruby_memcheck (~> 1.0, development)
  simplecov (~> 0.21, development)
  Used by
    rails-dom-testing-2.0.3 (nokogiri (>= 1.6))
    loofah-2.15.0 (nokogiri (>= 1.5.9))
    actiontext-7.0.2.3 (nokogiri (>= 1.8.5))
    xpath-3.2.0 (nokogiri (~> 1.8))
    capybara-3.36.0 (nokogiri (~> 1.8))
    reverse_markdown-2.1.1 (nokogiri (>= 0))

参考

qiita.com

Ruby on Rails: `strict_loading!`をしたPolymorphicなModelからassociationを辿ると`ArgumentError`が発生する

タイトル通り、かなりハマったので事象をメモしておく。

事象

タイトル通りですが、strict_loading!をしてeager_loadingを強制しているPolymorphic関連を持つモデルから、 関連を辿ると本来であればActiveRecord::StrictLoadingViolationErrorが発生すると思うのですが、 以下の通りArgumentErrorが発生します。

ArgumentError: Polymorphic associations do not support computing the class.
  activerecord-7.0.2.3/lib/active_record/reflection.rb:417:in `compute_class'
  activerecord-7.0.2.3/lib/active_record/reflection.rb:376:in `klass'
  activerecord-7.0.2.3/lib/active_record/core.rb:241:in `strict_loading_violation!'
  activerecord-7.0.2.3/lib/active_record/associations/association.rb:220:in `find_target'
  activerecord-7.0.2.3/lib/active_record/associations/singular_association.rb:44:in `find_target'
  activerecord-7.0.2.3/lib/active_record/associations/association.rb:173:in `load_target'
  activerecord-7.0.2.3/lib/active_record/associations/association.rb:67:in `reload'
  activerecord-7.0.2.3/lib/active_record/associations/singular_association.rb:11:in `reader'
  activerecord-7.0.2.3/lib/active_record/associations/builder/association.rb:104:in `target'
  active_record_gem.rb:46:in `test_strict_loading_to_Polymorphic_model'

再現コードはこちら

github.com

解決策

とりあえずstrict_loading!(false)して、strict_loadingを無効化してあげれば例外発生自体は防げるのですが、ArgumentErrorになるのは謎ですね。。。 ※ strict_loadingってPolymorphic associationに対応していない?

おわりに

strict_loadingとPolymorphic associationの掛け合わせなので、あんまりハマることも少ないかもですが。。。Rails難しい。。。 strict_loading使ってたけど、まだ本番利用してるところは少ないのかも?

2022/04/17追記

issueあげてみました

github.com

2022/05/07追記

対応版のPRがマージされて7.0 stableにバックポートされそうなので次回リリースで修正されそうです🙏✨

github.com

参考

madogiwa0124.hatenablog.com

Ruby on Rails: AikotobaというEmail/Passwordによるシンプルな認証ライブラリを作成しました

認証周りの勉強がてらAikotobaというEmail/Passwordによるシンプルな認証ライブラリを作成しましたので使い方とかをメモしておきます。

rubygems.org

Aikotobaとは?

Rails engineを使ったEmail/Passwordによるシンプルな認証ライブラリを作成できるライブラリです。

github.com

使い方は簡単で以下をGemfileに追記し、bundle install後に

gem 'aikotoba'

Rails engineをmountし、

Rails.application.routes.draw do
  mount Aikotoba::Engine => "/"
end

これだけでシンプルなEmail/Passwordによる会員登録及び認証の機能が使えるようになります。

f:id:madogiwa0124:20220312105558p:plain

f:id:madogiwa0124:20220312105610p:plain

あとは以下のような感じで認証用のHelperやログイン状態での認可用のメソッドを生やすとControllerやViewで分岐したり色々できるようになります。

class ApplicationController < ActionController::Base
  include Aikotoba::Authenticatable

  # NOTE: You can implement the get authenticated account process as follows.
  alias_method :current_account, :aikotoba_current_account
  helper_method :current_account

  # NOTE: You can implement the authorization process as follows
  def authenticate_account!
    return if current_account
    redirect_to aikotoba.new_session_path, flash: {alert: "Oops. You need to Signed up or Signed in." }
  end
end

またオプションで以下のような機能も利用することができます。

  • アカウント作成後にメール配信してEmailの確認を行う(DeviseのConfirmableっぽいやつ)
  • 指定回数ログインに失敗した場合にアカウントをロックしアンロック用のURLを配信する(DeviseのLockableっぽいやつ)
  • パスワード変更用のURLを配信しパスワード変更を行う(DeviseのRecoverableっぽいやつ)

詳しい使い方はREADMEを参照してください。

github.com

作成したモチベーション

作成したモチベーションは以下の用な感じです。

  • 単純に認証周りの考慮すべきセキュリティ等々の実装の勉強をしたかった。
  • 認証用のライブラリ、汎用性を考慮してる反面どれも実装が複雑で難しくなにか起きたときにモンキーパッチを当てて対応する等が難しかったので、Rails engineを使ったガッツリRailsに依存したシンプルな実装のライブラリがほしかった。
  • 既存の認証用のライブラリだとbcryptが使われていることが多いかモダンなHashアルゴリズムを使いたかった(Argon2)
  • Devise等デフォルトだとガッツリUserといったサービスのコアなドメインで認証用のロジックが含まれてしまうので、それを別のモデルにできるような設計にしたかった。
  • 複数DB対応をデフォルトで考慮したい。

おわりに

個人的には割と読みやすくシンプルに実装できたのと勉強にもなり個人のWebサービスにも導入できたので満足しました🥳

Ruby on Rails: Rollbarに連携する時に属性値等々をマスキングするMEMO

エラー通知サービスにRollbarを使っているのですが、ActiveRecord::RecordInvalid等の例外が発生したりするとActiveRecordインスタンスの属性等が表示されてしまいものによっては連携したくない情報がRollbarに連携されてしまう可能性があります。

Rollarのコード上の設定をいじると、この辺をいい感じにマスキングできることを知ったのでメモ🗒

rollbar.com

Rollbar連携前にマスキングする方法

設定は簡単でinitializer等のRollbar.configureの中でconfig.scrub_fieldsconfig.scrub_headersにフィルタリングしたいkeyを設定してあげればフィルタリングして連携してくれます。 以下の例ではRailsfilter_parametersと同様のマスキングを行うようにしています。

Rollbar.configure do |config|
  # NOTE: 秘密にした方が良い情報はマスキングする
  # https://docs.rollbar.com/docs/ruby#scrubbing-items
  config.scrub_fields |= Rails.application.config.filter_parameters
end

一応デフォルトでも一定フィルタリングしてくれるようです。

By default, the notifier will "scrub" the following fields from payloads before sending to Rollbar :passwd :password :password_confirmation :secret :confirm_password :secret_token https://docs.rollbar.com/docs/ruby#scrubbing-items

※コードを見る限り正規表現でマッチすればフィルタリングしてくれるっぽい

Rollbar便利ですね✨

Ruby on Rails: Active Record Encryptionを使って属性値を暗号化するメモ

Ruby on Rails 7から導入されたActive Record Encryptionを使ってみたところ大分良さそうだったので使い方とかをメモしてきます🗒

Active Record Encryptionとは?

Active Record Encryptionは、Rails 7から導入されたActive Recordの新機能です。

特定の属性値をシンプルなDSLで暗号化して扱うことができます。

guides.rubyonrails.org

Active Record Encryptionを使ってみる

Active Record Encryptionの初期設定

Active Record Encryptionで暗号化を行うのに必要な秘密情報を以下のコマンドで生成しcredentialsに設定します。

bin/rails app:db:encryption:init

Add this entry to the credentials of the target environment:

active_record_encryption:
  primary_key: EGY8WhulUOXixybod7ZWwMIL68R9o5kC
  deterministic_key: aPA5XyALhf75NNnMzaspW7akTfZp0lPY
  key_derivation_salt: xEY0dt6TZcAMg52K7O84wYzkjvbA62Hz

これだけで使う準備はOKです🙆‍♂️

Active Record Encryptionで暗号化してみる

例えばユーザー作成後にトークンを送信して存在確認したい時に以下のような実装を行なったとします。

class Account::ConfirmationToken < ApplicationRecord
  belongs_to :account
  validates :token, presence: true
  validates :expired_at, presence: true

  scope :active, ->(now: Time.current) { where("expired_at >= ?", now) }

  after_initialize do |record|
    record.token ||= SecureRandom.urlsafe_base64(32)
    record.expired_at ||= 1.day.ago
  end
end

今回の例ではトークンの有効期限は1日でありハッシュ化は行わないという判断をしていますが、 DBに存在する値が何かしらの理由により漏れた場合に不正に存在確認がされてしまう恐れがあります。

このようなリスクを軽減するために以下のようにencrypts :nameのような形式で記載するだけで暗号化して保存することができます。 ※deterministic: true決定論的暗号化を使用するフラグ値です。一意制約をDBに設定するケース等、同一の値を常に同一の暗号化結果とするためにはdeterministic: trueを設定します。

class Account::ConfirmationToken < ApplicationRecord
  belongs_to :account
  validates :token, presence: true
  validates :expired_at, presence: true

  encrypts :token, deterministic: true

  scope :active, ->(now: Time.current) { where("expired_at >= ?", now) }

  after_initialize do |record|
    record.token ||= SecureRandom.urlsafe_base64(32)
    record.expired_at ||= 1.day.ago
  end
end

以下のように${name}_before_type_castで実際にDBに保存されている値を閲覧できますが、アプリケーション内で使う分には暗号化されていることを気にせず利用することができます。

Account::ConfirmationToken.create(account: @account, token: "generated_token_1")
token = Account::ConfirmationToken.find_by(token: "generated_token_1")
token.token
# => "generated_token_1"
token.token_before_type_cast
# => "{\"p\":\"4P8OBmTH31wihhJq++M1iuU=\",\"h\":{\"iv\":\"Wox4jyGiHXpaaEXN\",\"at\":\"4FZ5dHQFKGnBJacTK0PttA==\"}}"

1行追加するだけで利用できて非常に便利ですね✨

Tips: 暗号化に必要な秘密情報をconfigで管理する

Active Record Encryptionは非常に便利なのですがcredentialsに暗号化に必要な秘密情報を格納する方式ではテスト環境等で少し扱いが困るかなと思ったのですが、 以下のようにconfigでも管理できるのでテスト環境とか開発環境ではconfingの値を使ってcredencialsは修正しなくても使えるようにも出来ます。

module Dummy
  class Application < Rails::Application
    config.active_record.encryption.primary_key = "foo"
    config.active_record.encryption.deterministic_key = "bar"
    config.active_record.encryption.key_derivation_salt = "baz"
  end
end

6 Configuration

  • primary_key: The key or lists of keys used to derive root data-encryption keys. The way they are used depends on the key provider configured. It's preferred to configure it via a credential > active_record_encryption.primary_key.
  • deterministic_key: The key or list of keys used for deterministic encryption. It's preferred to configure it via a credential active_record_encryption.deterministic_key.
  • key_derivation_salt: The salt used when deriving keys. It's preferred to configure it via a credential active_record_encryption.key_derivation_salt. Active Record Encryption — Ruby on Rails Guides

参考

techracho.bpsinc.jp

Ruby on Rails: Request specでresponse bodyのBOM付きCSVを検証する。

BOM付きのCSVダウンロード系の機能の検証をRequest specでresponse bodyのBOM付きCSVを検証する時に若干ハマったのでやり方をメモ🗒

BOM付きのCSVを検証する時の注意点

当たり前といえば当たり前ですが、encoding: 'bom|utf-8'を指定しないとパース時にエラー(Illegal quoting in line 1. CSV::MalformedCSVErrorのような)になるので注意が必要です。

stackoverflow.com

さらにパースできても以下のようにBOMが入っているのでkeyのマッチができなくなります。※ rows['column']が一見keyが一致しているように見えてもBOMによりnilが返却されます。

github.com

実運用ではエクセル等で文字化けせずに利用できて便利ですが、テストコードでBOM付きCSVを扱うのは大分大変です。

いい感じに検証する

上記の通りBOM付きCSVをそのまま扱うのは厳しいのでテストコードではBOMを削除して扱います。 response.body.delete("\uFEFF")でBOMを削除し、削除した文字列をそのままCSV.parseでパースして中身を検証しています。

describe 'GET /records.csv' do
  before { get records_path(format: :csv) }

  it '成功すること' do
    expect(response).to have_http_status(:ok)
  end

  it '適切なCSVが出力されること' do
    rows = CSV.parse(response.body.delete("\uFEFF"), headers: true).map(&:to_h)
    expect(rows.length).to eq 2
    expect(rows.last)).to eq(
      {
        'column1' => 'value1',
        'column2' => 'value2',
      }
    )
  end
end

※以下の通りBOMは容認されているが推奨されているものではんく削除によるリスクはほぼ無いものと判断しました。

UTF-8文字コードとしてASCIIを前提としたプログラムでもおよそ支障なく動作するように設計されているが、BOMによって正常に処理できなくなる場合がある。Unicodeの規格において、UTF-8においてBOMは容認されるが、必須でも勧められるものでもないとされている[5]。また、データベースやメモリにロードするデータなど、内部的なデータ形式では、プログラムの性能や効率の観点から普通BOMは用いられない。 バイト順マーク - Wikipedia

参考

qiita.com

Ruby: よりセキュアなArgon2でパスワードをハッシュ化するメモ

パスワードのハッシュ化といえば、RailsのActiveModel::SecurePasswordでもBcryptが使われているので今まで自分で作るときもBcryptを使っていたのですが、Bcryptには以下のような仕様があり、多少気になりArgon2を使ってみたので使い方とかをメモ📝

bcryptは、PHPのpassword_hash関数のデフォルトアルゴリズムである他、他の言語でも安全なハッシュ保存機能として広く利用されていますが、パスワードが最大72文字で切り詰められるという実装上の特性があり、その点が気になる人もいるようです(この制限はDoS脆弱性回避が目的です)。 bcryptの72文字制限をSHA-512ハッシュで回避する方式の注意点 | 徳丸浩の日記

Argon2とは?

Argon2 は 2015年の Password Hashing Competition で1位を獲得した比較的新しいハッシュ関数です。

  • GPU や専用のハードウェア(FPGA/ASIC) 攻撃に強い Argon2d
  • サイドチャンネル攻撃に強い Argon2i
  • Argon2d と Argon2i のハイブリッド型の Argon2id

OWASPに学ぶパスワードの安全なハッシュ化 | DevelopersIO

OWASAPでも基本的にはArgon2の使用が推奨されているようですね。

This cheat sheet provides guidance on the various areas that need to be considered related to storing passwords. In short

  • Use Argon2id with a minimum configuration of 15 MiB of memory, an iteration count of 2, and 1 degree of parallelism.
  • If Argon2id is not available, use bcrypt with a work factor of 10 or more and with a password limit of 72 bytes.

Password Storage - OWASP Cheat Sheet Series

Argon2をRubyで使う

Argon2をRubyで使用できる以下のRuby Argon2 Gemがあるのでそれを利用してみます💎

github.com

hasher = Argon2::Password.new
Argon2::Password.create("password")
    => "$argon2i$v=19$m=65536,t=2,p=1$61qkSyYNbUgf3kZH3GtHRw$4CQff9AZ0lWd7uF24RKMzqEiGpzhte1Hp8SO7X8bAew"

Version 2.0 - Argon 2id Version 2.x upwards will now default to the Argon2id hash format. This is consistent with current recommendations regarding Argon2 usage. It remains capable of verifying existing hashes.

Version 2.0以降であれば Argon2id hash formatでハッシュ化できるようです。

Ruby Argon2 Gemではハッシュ化に関するコストまわりの設定を行えますが、デフォルトだと以下のような設定になるようです。

  • m_cost (使用メモリ) : 16 ※2のm_cost乗のKiB値が使用されるため65536KiB(64MiB)がデフォルトの使用メモリ量になるようです。
  • t_cost (反復回数) : 2
  • p_cost(並列処理数) : 1

ref: ruby-argon2/test.c at b89f08dfffaf160dffcda1f9163e2e3e31076c9a · technion/ruby-argon2 · GitHub

OWASAPの推奨設定がデフォルトは以下のようなので、デフォルトは対象コストを上げているようですね👀

Use Argon2id with a minimum configuration of 15 MiB of memory, an iteration count of 2, and 1 degree of parallelism.

https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction

OWASAPの値に設定するなら以下のような感じになりそうです。

  • m_cost (使用メモリ) : 14※16384KiB(16Mib)
  • t_cost (反復回数) : 2
  • p_cost(並列処理数) : 1

Argon2でパスワードのハッシュ化・検証を行う

Argon2でパスワードのハッシュ化・検証を行う以下のようなclassを作ってみました。 そこまでBcryptと使い勝手は変わらず良い感じですね✨

require "argon2"

class Password
  def initialize(
    value:,
    pepper: "default-papper"
  )
    @value = value
    @pepper = pepper
  end

  attr_reader :value

  def match?(digest:)
    verify_password?(password_with_pepper(value), digest)
  end

  def digest
    generate_hash(password_with_pepper(value))
  end

  private

  def verify_password?(password, digest)
    Argon2::Password.verify_password(password, digest)
  end

  def password_with_pepper(password)
    "#{password}-#{@pepper}"
  end

  def generate_hash(password)
    # NOTE: Adjusted to be OWASAP's recommended value by default.
    # > Use Argon2id with a minimum configuration of 15 MiB of memory, an iteration count of 2, and 1 degree of parallelism.
    # > https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction
    argon = Argon2::Password.new(t_cost: 2, m_cost: 14, p_cost: 1)
    argon.create(password)
  end
end

password = Password.new(value: "password")
# => #<Password:0x00000001097b2208 @pepper="default-papper", @value="password">
password.digest
# => "$argon2id$v=19$m=16384,t=2,p=1$HyAICViZtYIldgmsu3mICw$7wfMkSRMnpyLzal1rdQ1YdnwsncfYceGXp4Cn7gDSSA"
password.match?(digest: password.digest)
# => true
other_password = Password.new(value: "other_password")
# => #<Password:0x0000000109f1f9a0 @pepper="default-papper", @value="other_password">
password.match?(digest: other_password.digest)
# => false