はじめに
こちらはOptTechnologiesの社内勉強会で利用した資料になります
調べた動機
ユーザからの問い合わせで、計測時に上がってくるURLが登録しているものと違うと言われたところから調査していたら、二重エンコードが問題になっていたことがありました
この時に、あ、俺URLエンコードよくわかってないな、と思ったのでいい機会だから調べてみようと思った所存です
URLエンコードとは
なんかよくわかんないけどマルチバイト文字っぽいのを%xx
の形に直してくれるやつ!
冗談はさておき。
URLエンコードとは
一般に世間で言われているURLエンコードには2種類のものがあります
- RFC3986にて規定されているUniform Resource Identifier(URI)の仕様の一部として定められているもの
- HTMLのFormで送信するデータ種類の1種である、
application/x-www-form-urlencoded
の仕様として定められているもの
つまり、単にURLエンコード
と称したときに、その意味は一意に定まらないようです・・・
RFC3986のURLエンコードについて
- 参考資料: https://triple-underscore.github.io/RFC3986-ja.html#section-2
- URIの定義として、利用可能な文字を定めている項があり、そこでエンコードも含めた文字表現として規定されている
- ざっくり以下のような定義になる
- 利用可能な文字は予約文字、非予約文字の2種類あり、これらは一定のルールに基づいてエンコードされたりされなかったりする(詳細は後述)
- それ以外の文字はすべてパーセントエンコードを実施する
パーセントエンコードについて
pct-encoded
= "%" HEXDIG HEXDIG
-
%XX
(Xは16進数の値)の3桁の値として表現 - 例えば、"%20"はスペースに該当する、など
- パーセントエンコードは文字列のバイト配列とのマッピングなので、文字コードにかかわらずエンコードが可能
- たとえば「ウィキペディア」を、各種の文字コードを用いてパーセントエンコーディングで符号化すると以下のようになる。
- Shift_JIS - %83E%83B%83L%83y%83f%83B%83A
- EUC-JP - %A5%A6%A5%A3%A5%AD%A5%DA%A5%C7%A5%A3%A5%A2
- UTF-8 - %E3%82%A6%E3%82%A3%E3%82%AD%E3%83%9A%E3%83%87%E3%82%A3%E3%82%A2
- (wikipediaより引用)
- たとえば「ウィキペディア」を、各種の文字コードを用いてパーセントエンコーディングで符号化すると以下のようになる。
利用可能な文字について
予約文字
reserved
= gen-delims
/ sub-delims
gen-delims
= ":" / "/" / "?" / "#" / "[" / "]" / "@"
sub-delims
= "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
- これらはURIで区切り文字などに利用される
-
gen-delims
はURI構成要素の区切り文字として機能する- 例:
http://example.com
の場合、プロトコル部とドメイン部を://
によって区切っている - その特性から、URI構成要素内でこれらの値を利用したい場合、条件に基づいてパーセントエンコードが必要
- 例えばクエリストリングのvalueで
?
を含む値の場合など - URI構成要素によって区切り文字として機能するかどうかが異なるので、
gen-delims
だからと言ってまるっとエンコードしていいわけではない
- 例えばクエリストリングのvalueで
- 例:
-
sub-delims
はURI構成要素の区切り文字以外で利用される区切り文字- 例えばクエリストリングのkeyとvalueを区切るために
=
が利用されている
- 例えばクエリストリングのkeyとvalueを区切るために
非予約文字
unreserved
= ALPHA / DIGIT / "-" / "." / "_" / "~"
- 自由に使って良い文字列とされている
- あるURIにおいて、これらの値をパーセントエンコードしたものとしていないものは等価として扱うという仕様が定められている
- 仕様上そう定められているが、アプリケーションがその同一性を担保してくれるかは実装によるため、URIを生成するアプリケーションはこれらの非予約文字をパーセントエンコードするべきではない
- 逆に、URIを解釈するアプリケーションはこれらの文字を必ずデコードするべきであるとも規定されている
エンコード/デコードについて
- 通常、エンコード処理はURI生成時にのみ実施される。生成されたURIは常にパーセントエンコードがされた状態であると規定されている
-
hoge=ふが
、foo=ばー
がクエリストリングとして付いたURIを生成するとして、http://example.com/?hoge=%E3%81%B5%E3%81%8C&foo=ばー
のような状態ではURIとして認められない
-
- URIから元の値を取り出したいという場合、デコード処理の前にURI構成要素を適切に分割する必要がある
-
foo=A&&B
,bar=C||D
がクエリストリングとして付いたhttp://example.com?foo%3DA%26%26B&bar%3DC%7C%7CD
をパースするときに、先にデコードしたらhttp://example.com?foo=A&&B&bar=C||D
になってしまうため
-
- エンコード/デコードは複数回してはいけない
- 元の文字列に
%
が含まれていた場合、これをパーセントエンコードの始まりと解釈しうるため -
金利2%20年ローン
=>金利2 年ローン
になってしまう - 自分が携わっている広告計測のプロダクトで、2重エンコードされたURLが来てしまう場合に2重デコードしてしまう案もあったが、上記のような問題が想定されたので実施はされなかったという経緯があったりなかったりしました
- 元の文字列に
以前のURIの仕様について
- RFC3986の前身にあたる仕様としてRFC2396というものがあり、こちらでも同様に
reserved
が定義されている -
reserved
の内容が異なる - encode系の実装がこの仕様に基づいていることがある
- JavaScriptの
encodeURIComponent
など - 参考: https://qiita.com/shibukawa/items/c0730092371c0e243f62 , http://freak-da.hatenablog.com/entry/20080321/p1
- JavaScriptの
application/x-www-form-urlencodedのURLエンコードについて
- 参考資料(WHATWGとW3Cどっちの仕様が正かあんまり把握してないのでとりあえず両方抜粋
- Form要素のsubmit時に送られるデータ形式の仕様として、parse/serializeの方法が定義されており、これがそれぞれデコード/エンコードの仕様にあたる
WHATWGにて定義されているエンコードのルール
資料: https://url.spec.whatwg.org/#urlencoded-serializing
byte serializer
として、以下のような処理を定義(概ねパーセントエンコード関数みたいなイメージ)
byte配列を入力とする
- 入力の全てのbyte配列に対し、以下の処理を実施
-
0x20 (SP)
ならU+002B (+)
を出力として追加 -
0x2A (*)
,0x2D (-)
,0x2E (.)
,0x30 (0) to 0x39 (9)
,0x41 (A) to 0x5A (Z)
,0x5F (_)
,0x61 (a) to 0x7A (z)
なら、そのまま出力として追加 - それ以外ならパーセントエンコードしたものを出力として追加
-
- 処理が完了したら、それを結果として出力する
WHATWGにて定義されているエンコードのルール
application/x-www-form-urlencoded serializer
を定義する(これがURLエンコードとして機能することになる)
[name-value]か[name-value-type]を要素とするリスト
及び、文字エンコーディング上書き設定
を入力とする
- デフォルト文字エンコーディングをUTF-8とする
- もし
文字エンコーディング上書き設定
があれば、そのエンコードを採用する - 以下の処理を
[name-value]か[name-value-type]を要素とするリスト
の全ての要素に対して実施する- byte serializerを
name
要素に対して適用したものをnameとする - もし
value
が、type
を持つ場合、- もし
type
がhidden
かつ、name
が_charset_
の場合、value
を文字エンコーディングで上書きする - もし
type
がfile
ならvalue
をvalue
のファイル名で上書きする
- もし
- byte serializerを
value
要素に対して適用したものをvalue
とする - 今処理している要素が最初の要素ではないなら、出力に
U+0026 (&)
を追加する - 出力に
name
+U+003D (=)
+value
を追加する
- byte serializerを
- 処理が完了したら、それを結果として出力する
application/x-www-form-urlencodedについて
- 空白が
+
になるのはこいつのエンコードの仕様。ファッキン - 歴史的経緯によりこのような複雑な仕組みになっているとのこと
-
<input type="hidden" name="_charset_">
とか初めて知った・・・
所感とか
- SPAの文脈ではRFC3986のことだけ知っておけば概ね生きていけそう
- でもURLエンコードという単語が一意に定まらないことは知っておかないとハマりそう
- RFC3986の仕様の方はURIを扱う時に気を付けるべきことについても知ることが出来るので、Web開発してる人は読んでおいて損はないと思った
- URLのencode/decodeの処理は実装によってどの仕様に基づいているかが異なるので、そこだけは気を付けるべし
- ブラウザの世界観が複雑で辛い案件をまた一つ知ってしまった