ソースマップのことを何もわかっていなかったので調べた
CATEGORY:技術
TAGS:#JavaScript
仕事でソースマップのことをちゃんと調べなきゃいけない状況になったので調べることにしたのでその調査メモです。
発端
Next.js に Sentry を導入するということをやっている。
導入自体はとても簡単で、@sentry/nextjs
というパッケージを使うことで実現できる。
@sentry/nextjs
には withSentryConfig
という関数があり、これによって next.config.js を拡張できる。そしてその設定の中に、hideSourceMaps
という設定があり、これを有効化することで Sentry 側にソースマップを渡しつつも本番環境ではソースマップが見えないようにできる。
hideSourceMaps
オプションの公式サイトの説明を読んでみると、このオプションを有効にすると Webpack の hidden-source-map
を有効にした状態になるみたい。
hidden-source-map
に関する Webpack の公式サイトの説明を読んでみると、ソースマップをエラーレポートのためだけに使用する場合に選択できる、と書いてある。
そして問題はここからで、実際にこれによって起こる挙動としては、ビルドされた JS ファイルにソースマップへの参照が記述されなくなるということで、ソースマップファイル自体が本番環境で削除されているわけではないということ。
例えば、ビルドされた結果の hoge.js
があったとして、普通にソースマップを作るようにすると hoge.js.map
というファイルが生成され、hoge.js
には hoge.js.map
への参照が記述される。
hideSourceMaps
を有効にすると、hoge.js.map
は作られるが、hoge.js
には hoge.js.map
への参照が記述されないため、ブラウザの developer tool で hoge.js
の中身を見ても、ビルド後の状態しかわからず、元々の開発時のコードの状態はわからなくなる。
ただここで僕が気になったのが、hoge.js.map
自体は存在して本番環境にも配置されているので、URL で直接 hoge.js.map
を叩けばその中身が見られる状態になっているということ。
ここが僕の理解の及ばない部分なのだけれど、hoge.js
と hoge.js.map
という2つのファイルの中身が見られるのであれば、なんらかの手段を使って結局開発時のコードの状態に復元できてしまうのではないか、それはセキュリティ的に問題あるのではないか、ということを考えた。
結局のところ、hideSourceMaps
の設定を使ったところで、ビルドフローの最後に自分で .map
ファイルを削除する処理を入れる必要があるのではないか、でも本当にそうなのか?というところがわからないので調べてみよう、というのが今回の記事に至った発端になる。
そもそもなぜソースマップが必要なのか
ここで少しおさらいのような感じになるが、そもそもなんでソースマップというものが必要になるのかというのを振り返る。
昨今のフロントエンドの開発では、TypeScript でコードを書いてそれをコンパイルした JS がブラウザで動いていたり、自分が書いた JS のコードがブラウザで動く時は圧縮・難読化されたり、みたいなことが行われている。
そうなってくると、ブラウザで動いているコードをデバッグしたいときに、圧縮後・難読化後のコードを見てもどこで何が行われているのか何もわからない、エラーが発生してもエラー発生箇所のコードが自分の書いているコード上のどの部分にあたるのかが全くわからない、ということになる。
それを防ぐためにソースマップを吐き出すことで、ブラウザは元々のコードの状態を取得でき、元々のコードの状態でのデバッグが可能となる。
もう一点、エラーレポートの関連でもソースマップは必要になる。
本番環境のアプリケーションでエラーが発生した際、Sentry のようなエラーレポーティングツールを使ってエラーの詳細をログとして残す、というのはよくやることだと思うが、その際にコードのどの部分でエラーが発生したかというのも重要な情報になる。
本番環境に置かれるビルド後の JS ファイルももちろん圧縮・難読化されるため、これもソースマップを使うことでエラーレポートの際に元々のコードのどの部分でエラーが起きたかをわかるようにできる。
ここらへんがソースマップが必要になる理由。
そもそもなぜ本番環境でソースマップを消したいのか
ではそもそも元々のコードを JS で書いて、圧縮や難読化をせずに本番環境に公開すればソースマップなんてものは必要にならないのではということを考えてみる。
それだとファイルサイズはでかくなるし第三者がそのコードをブラウザから見ることでセキュリティに問題のある部分を見つけやすくなるし、それはできないよね、という話がすぐにわかると思う。
ソースマップを本番で消したいという話もそのセキュリティの部分の問題で、ソースマップで元の状態のコードがわかってしまうのは危ないですよね、というのが理由になる。
ソースマップの仕様について
ソースマップファイルの中身を見たことがある人はわかると思うが、Base64 エンコードされた文字列が羅列されており、ぱっと見読み込むのは不可能だと感じる威圧感がある。
ただ、これはソースマップの仕様をしっかりと読むと意外とそこまで難しくないことがわかる。
ソースマップ仕様 (v3)
これを見ると、.map
ファイルの仕様はわりとシンプルな JSON であることがわかる。
JSON の形はこんな形になっている。
{
"version": 3,
"file": "out.js",
"sourceRoot": "",
"sources": ["foo.js", "bar.js"],
"sourcesContent": [null, null],
"names": ["src", "maps", "are", "fun"],
"mappings": "A,AAAB;;ABCDE;"
}
- version: ソースマップの仕様のバージョン
- file: ビルドされたファイル。
.map
ファイルはこのファイルの内容を示す - sourceRoot: sources の中のファイル群のベースパスみたいな感じ
- sources: オリジナルのファイルとそこから参照される関連ファイルの一覧が入る。mappings で使用されたオリジナルソースのリストでもある
- sourcesContent: sources のリストの各ファイルの中身。sources のファイル自体をホストできない場合はここにファイルの中身がそのまま入ってくる
- names: mappings で使用されるシンボル名のリスト
- mappings: エンコードされたマッピングデータ
この mappings がいつもすごいことになっているのでソースマップを見る気がしないんだけど、よく見ると sourcesContent
にそのままオリジナルのソースコードが入っている。
そして sourcesContent が null になっている場合は、オリジナルのファイル自体がホストされているはずなので、ブラウザに URL を直打ちすることでソースコードを取得できるようになっているはず。
結論
ことの発端となった @sentry/nextjs
の hideSourceMaps
オプションを使えば .map
ファイルへの参照が消えるので、本番環境にホストされた.map
ファイル自体を消す必要がないか、という疑問に関しては、だめですよというのが結論だった。
普通にソースマップを見れば元のファイルがわかってしまうので悪意のある人がそれを見て何か穴を探そうとする行為ができてしまうので、やはり結局ビルドフローの中で .map
ファイルは消す必要があった。
最初からソースマップの中身をしっかりよく見てれば一瞬でわかることだったのでなんだか悲しい気持ちになりました。
おわり。