Edited at

VCR で外部 API へのリクエストをダンプするときに機密情報をマスクしたい


はじめに

外部API への通信内容を yaml で記録し、モックを作りやすくする vcr が便利でよく使っています。

その中で、機密情報を伴うリクエストを vcr で取り扱うとき、機密情報をマスクする方法について調べました。


機密情報をマスクしたい

POST でユーザー名・パスワードをパラメータで送信することを例とします。

vcr を使うと、リクエストの内容が以下のような yaml ファイルで保存されるのですが、その際リクエストパラメータに username=xxxx&password=yyyy というように、機密情報が平文で残ってしまう課題がありました。

- request:

method: post
uri: https://test.api.com/auth
body:
encoding: US-ASCII
string: grant_type=password&username=xxxx&password=yyyy
headers:
User-Agent:
- Faraday v0.12.2
Content-Type:
- application/x-www-form-urlencoded
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"

外部に漏れても問題ない情報であればいいのですが、そうでない場合もあると思うので、なるべくマスクできればいいなと考えていました。


filter_sensitive_data を使えばできる

調べたところ、filter_sensitive_data というメソッドがあるようでした。

vcr の config に設定を記載することで、マスクしたい文字列にマスクを掛けることができます。

以下の例ですと、

VCR.config do |c|

c.stub_with :fakeweb
c.cassette_library_dir = 'cassettes'
c.filter_sensitive_data('<USERNAME>') { 'xxxx' }
c.filter_sensitive_data('<PASSWORD>') { 'yyyy' }
end

先ほどの yaml は xxxxyyyy の部分がそれぞれマスクされます。xxxxyyyy は、ENV['USERNAME'] や、Rails.application.secrets.username などの定数を割り当てて使うことが多いかなと思います。

- request:

method: post
uri: https://test.api.com/auth
body:
encoding: US-ASCII
string: grant_type=password&username=<USERNAME>&password=<PASSWORD>
headers:
User-Agent:
- Faraday v0.12.2
Content-Type:
- application/x-www-form-urlencoded
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"


filter_sensitive_data は文字列完全一致が必要そう

しかし、filter_sensitive_data では文字列完全一致をさせる必要があるというところで少しハマりました。

例えば、URL の中にリクエストパラメータがある場合、パラメータ内の文字列がエスケープされている場合があります。example@example.com をマスクしたい場合を考えます。

VCR.config do |c|

c.stub_with :fakeweb
c.cassette_library_dir = 'cassettes'
c.filter_sensitive_data('<USERNAME>') { 'example@example.com' }
end

しかし、URL 内のユーザー名にあたる文字列は、example%40example.com とエスケープされているため、filter_sensitive_data とマッチせず使えませんでした。。

# 中略

- request:
method: post
uri: https://test.api.com/auth
body:
encoding: US-ASCII
string: grant_type=password&username=example%40example.com
# 中略

このように、 URL 内でエスケープされている文字列は、エスケープしてあげる必要がありました。例としては以下のようになります。

VCR.config do |c|

c.stub_with :fakeweb
c.cassette_library_dir = 'cassettes'
c.filter_sensitive_data('<USERNAME>') { CGI.escape('example@example.com') }
end


ソースコード確認

上記の挙動について、念の為該当のソースコードも確認しました。


filter_sensitive_data について

https://github.com/vcr/vcr/blob/221abe52989993865bde4c52a92ee12bb1eb6141/lib/vcr/configuration.rb#L225-L238



  • filter_sensitive_datadefine_cassette_placeholder に alias で登録されている


  • define_cassette_placeholder が実行され、interaction.filter!(orig_text, placeholder) でオリジナルテキストと置き換え後テキストのフィルタリングが行われる


filter! について

https://github.com/vcr/vcr/blob/221abe52989993865bde4c52a92ee12bb1eb6141/lib/vcr/structs.rb#L557-L581



  • text, replacement_text がどちらも to_s されている


  • filter_object! 呼ばれる


  • object.gsub!(text, replacement_text) if object.include?(text) で、置き換え前のテキストが含まれていれば置換処理する

上記から、実際の置換処理は gsub! で実行されていますが、置換前・置換後の対象はどちらも文字列になってるので、完全一致させる必要がありそうでした。


おわりに

使い方をドキュメントやソースコードから追って確認するのは勉強になりますね。継続して使い方を確認しようと思います。


参考