URLのクエリーパラメータでエスケープしなくていい文字はどれか？

http://example.com/path?k1=v1&k2=v2
においてk1, v1, k2, v2の部分に使える文字について調べてみた。

# まとめ

- `*-._`の4文字と英数字はそのまま
- スペースは`+`に置換する
- それ以外はパーセントエンコードする

根拠：[HTML5のw3ｃの仕様がそう言っている。](https://qiita.com/shibukawa/items/c0730092371c0e243f62#comment-418b170c3037c6bf1e64)

- JSの`encodeURIComponent`はこの4文字に加えて`!'()~`もエスケープから除外している。`encodeURIComponent`はRFC 2396準拠であり、古い。よって`encodeURIComponent`が常に正かというとそういうわけではない。
- 現実には、スペースを`+`でなく`%20`に置換してしまっても問題ない事が多い
- それどころか、`*-._` の4文字もパーセントエンコーディングしてしまって問題ない事が多い


# 調査記録

- RFCを追っていくと、パーセントエンコードしなくていいのは`-._~`の4文字と解釈できそうだった。これはHTML5の仕様と微妙に違う。でもおそらくHTML5の仕様を信用した方がいいと思う。
    - クエリーパラメータの仕様を定めているのはRFC 1866 x-www-form-urlencodedである。
    - RFC 1866は「スペースを`+`に変換し、URLで予約された文字をパーセントエンコードする」と定義している。
    - RFC 3986によれば、unreservedと定義されているのは`-._~`の4文字。
- 現実には、スペースは`%20`、`+`どちらになる場合もある。
    - x-www-form-urlencodedの仕様的には`+`の方が正しいのかもしれないが、現実的には`%20`にした方が安全と思う。
        - `%20`なら受け取る側はまず間違いなくスペースと解釈するだろうから。
        - curlも`--data-urlencode 'hoge= '`では`%20`にする。
    - JSの`encodeURIComponent`は`%20`にするし、`decodeURIComponent`は`+`をスペースに置換しない。
        - `encodeURLComponent`が準拠するのはRFC 3986でなくRFC 2396である。つまり古い。RFC 1866は考慮していないと言える。
- 関連RFCは
    - RFC 2396 "Uniform Resource Identifiers (URI): Generic Syntax" （1998年発行）
    - RFC 3986 "Uniform Resource Identifiers (URI): Generic Syntax" （2005年発行）
        - 2396は「Obsoleted by: 3986」なので現在は3986だけを参照すればよい
    - RFC 1866 "Hypertext Markup Language - 2.0" 8.2.1. The form-urlencoded Media Typeの部分
- encodeURI と decodeURI は URI 全体に適用するもので、encodeURIComponent と decodeURIComponent は URI の要素に適用するものと意図されている。
- Chromeのアドレスバーで「python ruby」と入れて検索するとURLは「python+ruby」になる

# RFC 1866

>8.2.1. form-urlencoded メディアタイプ(The form-urlencoded Media Type)
>
>すべてのフォームのデフォルトエンコーディングは`application/x-www-form-urlencoded'です。このメディアタイプではフォームデータセットは以下のように表わされます。
>
>1.フォームフィールドの名称と値は次のようにエスケープされます：**空白文字は'+'に置き換えられ、予約された文字記号は[URL]に従ってエスケープされます。すなわち、英数字以外は'%HH'――パーセント記号と記号のASCIIコードを１６進数で表わしたもの――に置き換えられます。**複数行のテキストフィールドにおいて、改行はCRとLFの組み合わせ、すなわち'%0D%0A'で表わされます。
>
>2.フィールドは文書に現われる順番に、'='により値と名称を区別し、'&'によりほかの組み合わせと区別して並べられます。値のないフィールドは無視されるでしょう。特に選択されていないラジオボタンやチェックボックスは符号化されたデータには現われませんが、VALUE属性を持つ隠しフィールドは現われます。
>
>注意‐問い合わせフォーム提出からのＵＲＩは通常のハイパーリンクのアンカー形式として使用できます。不運なことに、'&'記号をフィールドの区切りに使用することは、ＳＧＭＬ属性値において実体参照区切り子としてその記号を使うことと互いに影響を与えあいます。例えば、`http://host/?x=1&y=2`は、`<a href="http://host/?x=1&#38;y=2">`あるいは`<a href="http://host/?x=1&amp;y=2">`と書くべきです。
>
>ＨＴＴＰサーバ、特にＣＧＩはユーザーをトラブルから守るために、'&'の代わりに';'の使用をサポートすることを薦めます。

# RFC 2396

```
RFC2396 Appendix A. Collected BNF for URI (抜粋)

URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
absoluteURI   = scheme ":" ( hier_part | opaque_part )
hier_part     = ( net_path | abs_path ) [ "?" query ]
query         = *uric
uric          = reserved | unreserved | escaped
reserved      = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
                "$" | ","
unreserved    = alphanum | mark
mark          = "-" | "_" | "." | "!" | "~" | "*" | "'" |
                "(" | ")"
escaped       = "%" hex hex
```

reserved（予約文字）はURIにおいて特殊な意味を持つ文字。よってこれらをパラメータで送りたい場合は`?q=&`でなく`q=%26`のようにURLエンコードしなければならない。

# RFC 3986

```
   unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
   reserved      = gen-delims / sub-delims
   gen-delims    = ":" / "/" / "?" / "#" / "[" / "]" / "@"
   sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
                 / "*" / "+" / "," / ";" / "="
```

RFC 2396にはあったreservedという文言がなくなっているが、sub-delims、":"、"@"がreservedに相当する。よって`!'()*`の五文字が、以前はunreservedであったが現在はreservedになった、ということ。

# JavaScriptのencodeURIComponent

RFC 2396の通り、9個の文字がエスケープされずに残っている。

```
        %20
!       !       equal
"       %22
#       %23
$       %24
%       %25
&       %26
'       '       equal
(       (       equal
)       )       equal
*       *       equal
+       %2B
,       %2C
-       -       equal
.       .       equal
/       %2F
:       %3A
;       %3B
<       %3C
=       %3D
>       %3E
?       %3F
@       %40
[       %5B
\       %5C
]       %5D
^       %5E
_       _       equal
`       %60
{       %7B
|       %7C
}       %7D
~       ~       equal
```

上記結果出力用のスクリプト

```urlencode.js
function encodeString(str) {
    for (var j = 0; j < str.length; j++) {
        var c = str[j];
        console.log(c + "\t" + encodeURIComponent(c) + "\t" + (c === encodeURIComponent(c) ? "equal" : ""));
    }
}

for (var i = 0x20; i < 0x7F; i++) {
    var s = String.fromCharCode(i);
    if (!s.match(/[A-Za-z0-9]/)) {
        encodeString(s);
    }
}
```

# RubyのURI.encode_www_form_componentとencode_www_form

HTML5のw3ｃの仕様と同じく`*-._`の4文字をそのまま残している。

```
 	+	
!	%21	
"	%22	
#	%23	
$	%24	
%	%25	
&	%26	
'	%27	
(	%28	
)	%29	
*	*	equal
+	%2B	
,	%2C	
-	-	equal
.	.	equal
/	%2F	
:	%3A	
;	%3B	
<	%3C	
=	%3D	
>	%3E	
?	%3F	
@	%40	
[	%5B	
\	%5C	
]	%5D	
^	%5E	
_	_	equal
`	%60	
{	%7B	
|	%7C	
}	%7D	
~	%7E	
```


```rb
require "uri"

(0x20...0x7F).each do |c|
  s = c.chr
  unless s =~ /[A-Za-z0-9]/
    encoded = URI.encode_www_form_component(s)
    puts "#{s}\t#{encoded}\t#{s == encoded ? 'equal' : ''}"
  end
end
```

# 参考

- [技術/HTTP/URLエンコードで 0x20(スペース) を "+" にすべきか "%20" にすべきか](https://www.glamenv-septzen.net/view/1170#idb783f7)
- [情報セキュリティ技術動向調査（2009 年下期）：IPA 独立行政法人 情報処理推進機構](https://www.ipa.go.jp/security/fy21/reports/tech1-tg/b_09.html)
- [encodeURIComponentが世界基準だと誤解してた話 - Qiita](https://qiita.com/shibukawa/items/c0730092371c0e243f62)