freeeの開発情報ポータルサイト

より良いプロダクトを目指して、freee 請求書のメール送付機能不正利用防止対策の話

こんにちは!freee 債権請求書領域でプロダクト開発に取り組んでいる 🇰🇷 韓国出身のエンジニア jason です。
この記事は freee Developers Advent Calendar の24日目🎄になります。

adventar.org

すでにお気づきの方もいらっしゃるかもしれませんが、今年のアドベントカレンダーでは担当の25名の中で、債権請求書チームのエンジニアが5名も参加していて、朝会などで雑談ネタとして大いに盛り上がっています!

また、QA エンジニアである nori さんも参加していて、それぞれが興味深いテーマで構成されていますので、まだ読んでいない方は、ぜひご覧ください!

はじめに

債権請求書開発チームは、事業所が請求書を発行、送付してから入金と消し込むまでのプロセスをより簡単かつ効率的に進めるための機能開発に取り組んでおり、主に次の2つのプロダクト開発に携わっています。

  • freee 会計(自動で経理、自動登録ルール)
  • freee 請求書

新規機能の開発やユーザー要望・ハッピーの対応が通常の開発アイテムになりますが、ユーザーが安心して信頼できる SaaS プロダクトとなるようにさまざまな取り組みも実施しています。

その取り組みの中でもfreee 請求書のメール送付機能に対する取り組みをご紹介します。

取り組み紹介

背景

freee 請求書ではより多くのユーザーの請求業務をサポートするために、メール送付機能(一部機能制限あり)を無料で提供しています。また、ユーザーがより簡単かつ快適に機能を利用できるよう、機能改善にも積極的に取り組んでいます。

一方で、これらの機能が安全かつ安定的に利用できるようにすることも非常に重要であり、特にメール送付機能は無料で提供していることから、スパマー等による不正利用が発生するリスクが高いです。 さらに、不正利用により大量の悪質なメールが送付されてしまうと、メールサーバーから勝手に迷惑メールであると判断され、ユーザーが送付したい帳票が取引先に意図したタイミングで届かなくなる可能性があります。

このようなご迷惑をおかけしないため、freee では PSIRT や SRE をはじめとするさまざまなチームが全社レベルでの対策を入れていますが、今回はプロダクト側でも機能の安全・安定性を向上させるための対策を実施しました。

対策 ①:Rack::Attack を使って、API Rate Limit を適用

今回の対策に関する詳細な内部アルゴリズムや数値については明らかにしないですが、freee 請求書では Rack::Attack を使って Rate Limit を実装しました。

Rate Limit の情報を一目で簡単に把握できるように配列に入れ、Rate Limit の数値は環境変数として管理することで別途の Deploy をしなくてもをすぐ反映できるようにしました。

# config/initializers/rack_attack.rb

RATE_LIMIT_CONFIGURATION = [
  {
    key: "mail create rate limit per min",
    limit: MAIL_CREATE_API_MIN_LIMIT,
    period: 1.minute,
    path: "/api/mail",
    logic_method_name: :count_mail_api_request
  },
  {
    key: "mail create rate limit per day",
    limit: MAIL_CREATE_API_DAY_LIMIT,
    period: 1.day,
    path: "/api/mail",
    logic_method_name: :count_mail_api_request
  },
]

そして、下記のように Rack::Attack.throttle の中で send を使って logic_method_name のメソッドを呼び出すことで、API 別に Rate Limit 判断 Logic を組むことができるようにしました。

# config/initializers/rack_attack.rb

RATE_LIMIT_CONFIGURATION.each do |limit_config|
  Rack::Attack.throttle(limit_config[:key], limit: limit_config[:limit], period: limit_config[:period]) do | req |
    send(
      limit_config[:logic_method_name],
      req,
      limit_config[:limit],
      limit_config[:path],
      limit_config[:key]
    )
  end
end

# Rate Limit 判断に必用な Logic を実装
def count_mail_api_request(req, limit, path, key)
  request = ActionDispatch::Request.new(req.env)
  return nil unless req.path !== path

  # ...

  rate_limit_key
end
対策 ②:Middleware layer に logger を追加

Rails Logger は config/application.rb もしくは config/environments/* に設定され、Datadog に収集されています。 しかし、Rack::Attack の処理は Rails Logger が初期化される前の Middleware layer で動作してしまい、設定によっては Log data が Datadog に送信されない場合があります。

この問題を解決するために、freee 請求書では Rack::AttackCustomizing responses 機能を活用して対応しました。

Customizing responses は Rate Limit に引っかかった場合に Response を生成する機能であり、このタイミングで Logging に必要な Request Data や環境変数が揃っています。そのおかげで、下記のように簡単に Log data を生成することができました。

# config/initializers/rack_attack.rb

LOGGER = /* ... */
HTTP_STATUS_CODE = 429

Rack::Attack.throttled_response = lambda do |env|
  # Loggin に必用なデータを打ち込む
  LOGGER.error({
    system_message: 'Rate limit exceeded',
    ip_address: ip_address,
    http: {
      method: request.request_method,
      status_category: 'ERROR',
      status_code: HTTP_STATUS_CODE,
    },
  })

  # Response customize が可能
  [HTTP_STATUS_CODE, {}, ['Rate limit exceeded']]
end

この対応により、生成された Log は Datadog に確実に収集されるようになりました。

対策 ③:Monitoring 追加

Rate Limit に引っかかった Log が出力されたということは、不正利用の可能性がある Request が発生したことを意味します。 そのため、プロダクトチーム側でこれらが監視できるようにするために、Datadog Monitoring 設定を Terraform で構築し、監視体制を強化しました。

resource "datadog_monitor" "production_freee_invoice_rate_limit" {
  name  = "メール送付 Rate Limit 到達検知"
  type  = "log alert"
  query = "logs(\"service:freee-invoice @http.status_code:429 env:production\").index(\"*\").rollup(\"count\").last(\"XXm\") > 0"
  monitor_thresholds {
    critical = "0"
  }
  message = <<-EOT
    {{#is_alert}}
    @slack-${Slack チャンネル名}
    メール送付 Rate Limit 到達を検知しました。
    不正利用の可能性がないか確認してください。
    {{/is_alert}}
  EOT

  on_missing_data     = "default"
  require_full_window = false
}
対策 ➃:Email Address Validation API の導入

しかし、このような Rate Limit や Logging、Monitoring だけでは不十分なところがあります。

スパマーが何らかの手段で Rate Limit の許容範囲を突き止め、その範囲内で悪質なメールを送付し続けると、こうした行為が検知できない恐れがあります。 このようなメール送付が続くと、メール送付に使われている freee 請求書のメールアドレス(noreply@freee.co.jp)の信頼度(Email Reputation)が低下してしまい、 正常に利用しているユーザーの帳票メールが迷惑メールとして判定されるリスクも生じます。

これを防ぐために、Sendgrid が提供する Email Address Validation API を導入しました。

悪質なメールを送付しているスパマーは、ランダムまたは不正に生成されたメールアドレスを使用している可能性が高いです。 そのため、メール送付時にこの API でメールアドレスを検証を実施後、不正利用の可能性があると判定された場合、即座に対応できる仕組みを導入し、スパマーによる影響を最小限に抑えています。

この API 自体は PSIRT 側で Wrapper API を用意しており、検証結果を適切にキャッシュし、更新する仕組みを備えています。これにより、メール送付機能を利用するユーザーに遅延が発生しないよう配慮しています。

最後に

今回ご紹介した対策に加えて、さまざまな観点からさらなる施策を導入・検討し、より良いプロダクトを作るためにチーム一丸となって日々取り組んでいます。

freee の CEO である佐々木が語ったように、freee のみんなは 「ユーザーにマジ価値を届けきりたい」 という志を大切にしています。

どんなによいプロダクトをつくっても、それだけでは使われない。使われなければ、なんの価値も生まない。「優れたプロダクトをつくる技術」が必要なのと同様に、その価値を伝え「世の中を塗り替える技術」が必要だ。

freee に入社して約1年が経ち、日々の業務を通して、優れたプロダクトを作っても、それだけでは使われなければ意味がないと実感しています。 使われないことで価値が生まれないからこそ、技術力だけでなく、その価値を伝え、世の中を変える力も必要だと感じています。

もし、個人事業主や法人の方々が請求業務で困っている場合、ぜひ freee 請求書を検討していただき、ご利用いただきながらフィードバックをいただけると非常に嬉しいです。 そのフィードバックを基に、さらに改善を重ねていきますので、今後ともよろしくお願いいたします!