のらねこの気まま暮らし

技術系だけど、Qiita向きではないポエムとかを書くめったに更新されないヤツ

logone-jsにパスワードのマスク処理を追加した

logone-jsのアップデートを行ったのでブログとして残す。

logoneについては過去の記事を参照してね。 mizuki-r.hatenablog.com

リリース内容

新機能

  • シークレット文字列のマスク機能

不具合修正

  • maxmum call stackの修正

シークレット文字列のマスク機能

開発環境下でのデバッグログや、API通信のペイロードなどをログとして出力したいことは多くある。その中に認証情報やパスワードなどが含まれてしまうこともよくある。

次のようなコードは頻出パターンと言える。

try {
  const data = await axios.post('/authorize', { username, password })
catch (error) {
  logger.error(error.message, { error })
  throw error
}

この時、errorがrequestやresponseなどの情報構造を内包している場合(AxiosErrorとか)、意図せずデータがログに露出する可能性がある。 開発環境のみで動作するものであれば良いが、開発環境フラグを設定し忘れたり、エラーログの中に紛れこんで困ることがあるかもしれない。

RailsやLaravelといった大手のフレームワークで用いるロガーにはそのようなシークレットトークンに対してマスクするような機能が内包されていたりする。

今回logoneに追加したのは任意キーワードにマスクをかけられるようにする機能だ。

以下のような設定を行う。

const app = express()

app.use(logone({
  maskKeywords: [
    'password', // "password"というプロパティに一致する値をマスクする
    /password$/i, // 正規表現で大文字小文字を識別せずpasswordという文字列に後方一致する値をマスクする
    /password:¥s"(.+)"$/, // 'password: "XXXXXXX"' という文字列を検索し、'XXXXXXX'をマスクする
  ]
}))


app.post('/login', (req, res) => {
  req.logger.debug("request", { req })
})

文字列、正規表現のリストをパラメータとして受け取り、それにマッチするプロパティの値を同じ文字列長だけ*でマスクします。プロパティはpayloadのオブジェクトを再起的に探索します。messageに指定された値はmaskされません(が、まあログのメッセージにシークレットを書き出すような運用はしないでしょう...)

また、正規表現がキャプチャグループを持つとき、それはプロパティではなく値そのものから対象を検索します。 これは、HTTP通信のログなどで値がシリアライズされている場合もあるためこのようにしています。

マスク処理自体は単純なリプレイスです。

value.replace(/./g, '*')

maximum call stackの修正

前述の仕組みはpayloadというunkownなオブジェクトに対しての再起探索が必要になります。

今まさにlogoneを利用しているプロジェクトではHTTP通信にAxiosを利用しており、そのエラーオブジェクトは再帰的なオブジェクト構造を持ちます。

再帰構造を持つオブジェクトに再帰処理を行うと Maximum call stack exceededの例外が発生します。 まあ... 処理が終わらないので...

それ故に、JSONに置き換える処理では再帰参照を止めるように仕組みを作っていたんですが、それはAdapter側の出力の範囲でした。今回の対応はAdapterに渡す前のオブジェクトだったため、オブジェクト構造を判定して再帰構造を解体するようにしました。

まとめ

以下の2点を@logone/coreに追加したv0.0.4をリリースしました。 - 再帰的なオブジェクト探索によるパスワード文字列のマスク - 再帰構造を持つオブジェクトの解体

今回は必要に迫られて突貫で対応しましたが、パラメータ名などもうちょっと考慮の余地あったんじゃないかな〜と思ったり。

次は出力できるLogLevelの設定とかを追加したいですね。