Posted at

URLエンコードについておさらいしてみた

More than 1 year has passed since last update.


はじめに

こちらはOptTechnologiesの社内勉強会で利用した資料になります



調べた動機

ユーザからの問い合わせで、計測時に上がってくるURLが登録しているものと違うと言われたところから調査していたら、二重エンコードが問題になっていたことがありました

この時に、あ、俺URLエンコードよくわかってないな、と思ったのでいい機会だから調べてみようと思った所存です



URLエンコードとは


なんかよくわかんないけどマルチバイト文字っぽいのを%xxの形に直してくれるやつ!


冗談はさておき。



URLエンコードとは

一般に世間で言われているURLエンコードには2種類のものがあります


  1. RFC3986にて規定されているUniform Resource Identifier(URI)の仕様の一部として定められているもの

  2. 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だからと言ってまるっとエンコードしていいわけではない






  • sub-delimsはURI構成要素の区切り文字以外で利用される区切り文字


    • 例えばクエリストリングの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の仕様について



application/x-www-form-urlencodedのURLエンコードについて



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を持つ場合、


      • もしtypehiddenかつ、name_charset_の場合、valueを文字エンコーディングで上書きする

      • もしtypefileならvaluevalueのファイル名で上書きする



    • byte serializerをvalue要素に対して適用したものをvalueとする

    • 今処理している要素が最初の要素ではないなら、出力にU+0026 (&)を追加する

    • 出力にname + U+003D (=) + valueを追加する



  • 処理が完了したら、それを結果として出力する



application/x-www-form-urlencodedについて


  • 空白が+になるのはこいつのエンコードの仕様。 ファッキン

  • 歴史的経緯によりこのような複雑な仕組みになっているとのこと


  • <input type="hidden" name="_charset_">とか初めて知った・・・



所感とか


  • SPAの文脈ではRFC3986のことだけ知っておけば概ね生きていけそう


    • でもURLエンコードという単語が一意に定まらないことは知っておかないとハマりそう



  • RFC3986の仕様の方はURIを扱う時に気を付けるべきことについても知ることが出来るので、Web開発してる人は読んでおいて損はないと思った

  • URLのencode/decodeの処理は実装によってどの仕様に基づいているかが異なるので、そこだけは気を付けるべし

  • ブラウザの世界観が複雑で辛い案件をまた一つ知ってしまった