URL文字列のエスケープに御用心
はじめに
新しい話題ではありませんが、知っていると役に立つかもしれない小話です。
エスケープした結果が異なる?
URLのパス文字列やクエリ文字列をエスケープする処理、例えば#
を%23
に変換する処理が実装によって異なるのをご存じでしょうか?技術的に枯れていて差が発生しそうには思えませんが、その思い込みが罠でした。
以降はASCII文字のエスケープに限定して話を進めます。また、比較対象としてここではNodeJSとGoの実装を扱います。
それでは0x00
から0x7f
までの文字をエスケープして、その差を確認してみましょう。
NodeJSのencodeURIComponent
まずはNodeJSの標準ライブラリからencodeURIComponentの動作を確認しましょう。以下のコードを実行するとASCIIコードの16進数表記とエスケープの結果がタブ区切りで表示されます。
for (let i = 0; i <= 0x7f; i++) {
const input = `0x${i.toString(16)}`;
const output = encodeURIComponent(String.fromCharCode(i));
console.log(`${input}\t${output}`);
}
Goのnet/url.QueryEscape
続いてGoの標準パッケージからnet/url.QueryEscapeの動作を確認しましょう。こちらも実行するとASCIIコードの16進数表記とエスケープの結果がタブ区切りで表示されます。
package main
import (
"fmt"
"net/url"
)
func main() {
for i := rune(0); i <= rune(0x7f); i++ {
input := fmt.Sprintf("%x", i)
output := url.QueryEscape(string(i))
fmt.Printf("0x%s\t%s\n", input, output)
}
}
なおGoの場合、net/url.PathEscape関数も提供されています。比較対象のencodeURIComponentはURLに含まれる個別のコンポーネントをエスケープするのが目的ですので、ここではQueryEscapeを選びました。
エスケープ結果の比較
NodeJSのencodeURIComponentとGoのurl.QueryEscapeの差を以下の表に示します。
ASCIIコード | NodeJS | Go |
---|---|---|
0x20 | %20 |
+ |
0x21 | ! |
%21 |
0x27 | ' |
%27 |
0x28 | ( |
%28 |
0x29 | ) |
%29 |
0x2a | * |
%2a |
どのような状況で問題が発生するか
エスケープした結果に差が発生することは確認できました。続いて、どのような状況で問題が発生するか検討します。
NodeJS実装とGo実装が同居している
まず考えられる問題はNodeJSで実装されたシステムとGoで実装されたシステムが同居している状況です。
先ほどの比較表について空白文字0x20のエスケープに注目してください。NodeJSは%20
、Goは+
にエスケープされます。
エスケープの解除について、Goのurl.QueryUnescapeは%20
を0x20のASCII文字と解釈して空白文字に変換します。一方、NodeJSのdecodeURIComponentは+
をそのままプラス記号として出力します。
従ってシステム間でエスケープされた文字を介して通信する場合、NodeJSからGoは問題ありませんが、GoからNodeJSは意図しない結果が生じる恐れがあります。
とはいえ、この不具合はそれほど深刻な問題ではありません。?value=Hello%20World
は成功するのに?value=Hello+World
は失敗する、といった具合に成功か失敗かシステムの振る舞いを観測できるからです。
また、パス文字列やクエリ文字列を扱う処理にユニットテストを実装することで不具合をシステムが本番環境で稼働する前に発見できる可能性が高まります。
エスケープ解除前の文字列をそのまま扱っている
続いて、問題が発生していることに気がつく可能性が低い例を考えます。ぱっと思いつくのは以下2点でしょうか。
- CDNのキャッシュに利用するキーの値
- リクエストのログ分析でフィルタリングする際の条件
CDNが有効に機能していなくてもオリジンからレスポンスが返却されるため、設定の不備を見落とす恐れがあります。ログ分析する際のフィルタリングについては、多少の取りこぼしが許容される環境やサンプリングして一部のログだけ取り出す運用をする場合、フィルタ条件が機能していないことを見落とすかもしれません。どちらも致命的な問題ではありませんが、実装次第で思いがけない不具合を引き起こすかもしれません。
おわりに
技術的に枯れていそうだからといって、罠がないとは限りません。自分への戒めを込めた記事でした。
Discussion