Webサーバーの設定で、HTTPステータス毎に表示するページを調整することがあります。
Apacheなら .htaccess の ErrorDocument の設定、IISであれば Web.config の <system.webServer> の <httpErrors> の設定になりますね。
先日、お世話になっているお客様の環境で、Webサーバーの 400 エラーの応答を確認する必要があったのですが、400エラーを意図的に発生させる事に悩みました。
HTTP応答で、301, 302 や 401, 403, 404 などのステータスを確認をするためリクエストは、Webサイトの仕組みを知っていれば割と簡単に発生させることができます。
500エラーはアプリケーションエラーやサーバーの設定ミスですし、502, 503, 504 もARRなどのリバースプロキシが用意できれば発生させることができます。
ですが、400エラーってどうすれば出るの?という内容です。
400 エラー Bad Request とは?
そもそも、400エラーって何?というところですが、色々と検索して調べるとリクエストの構文が不正ということですが・・・
正確な情報をと思い、HTTPプロトコルの RFC2616 を見てみました。
www.w3.org, 400 Bad Request
The request could not be understood by the server due to malformed syntax. The client SHOULD NOT repeat the request without modifications.
Google翻訳に頼ったところ、
----------
構文が正しくないため、サーバーは要求を理解できませんでした。 クライアントは、変更せずに要求を繰り返すべきではありません(SHOULDNOT)。
----------
つまり、要求 (リクエスト) が不正だということは分かりました。
はい。!?・・・具体的にどこを不正にすれば 400エラー になるのか分かりません・・・
色々と検索してた際に、400エラーが発生して困っているという記事も見かけていて、ブラウザのキャッシュ削除とCookieの削除で解決するというものが多かったので、Cookieの値が不正なものになった場合に400エラーが発生するという推測はしていました。
400 エラー Bad Request を発生させる
実際に色々と試してみるしかないということで、色々とサーバーにリクエストを投げて確認してみました。
ブラウザでのURL指定のリクエストでは、ブラウザが正しいHTTPヘッダのリクエストを作ってしまうため、ブラウザは使用できません。
PowerShellを使用したWebリクエストの記事 PowerShellでWebリクエスト の方法でHTTPヘッダを組み立てて確認しました。
以下、PowerShellのコードのHTTPリクエストの部分だけ記載します。リクエストの末行には空白改行があると見なしてください。http:80番ポートへの平文リクエストです。
なお、確認したWebサーバーは、Windows Server 2016 の IIS (IIS 10)です。サーバーの内部IPアドレスは 172.24.0.31 です。
URLパスの末尾に % (%25) があると、400エラー
GET /%25 HTTP/1.1 HOST: 172.24.0.31
レスポンス結果
HTTP/1.1 400 Bad Request Cache-Control: private Content-Type: text/html Server: Microsoft-IIS/10.0 X-AspNet-Version: 4.0.30319 X-Powered-By: ASP.NET Date: Tue, 29 Dec 2020 15:06:48 GMT Content-Length: 33 要求が正しくありません
URL的には http://172.24.0.31/% ですが、% をURLエンコードすると %25 なので、ルートパスへのリクエストとしては /%25 になります。
%記号はURLエンコードを表す文字ですが、%25 は "%" 自体の文字のため、16進数2桁の文字が続くはずなのに、終わって切れている不正なURLになるので、400エラーが発生します。
なお、Webサイトルートの /%25 のURLでエラーさせてますが、/aaaa/bbbb/%25 など、フォルダパスがサーバー上に存在する/しないに関係無く、末尾が %25 の場合で400エラーが発生しました。
/%2500 の様に、%25 の後に2桁続いていれば、400エラーは発生しません。指定したファイルは無いので、404エラーになります。
HOSTヘッダー値が無いと、400エラー
GET / HTTP/1.1
レスポンス結果
HTTP/1.1 400 Bad Request Content-Type: text/html; charset=us-ascii Server: Microsoft-HTTPAPI/2.0 Date: Tue, 29 Dec 2020 15:08:19 GMT Connection: close Content-Length: 334
本来は、HOST: 172.24.0.31 やサイトのホスト名を指定するものです。このHOSTヘッダーを無くしてリクエストすると、400エラーが発生します。
この時のレスポンスは、上の /%25 の結果とは違い、Serverヘッダ値が Microsoft-HTTPAPI/2.0 と表示されていて、ASP.NETを示唆するヘッダーもありません。
Cookie値など、HTTPリクエストが異常に長いと、400エラー
GET / HTTP/1.1 HOST: 172.24.0.31 Cookie: a=aaaaaaaaaaaaaa.....[2万文字とか、ヘッダーの総文字数が16384文字を超える]
または、ユーザーエージェントなど
GET / HTTP/1.1 HOST: 172.24.0.31 User-Agent: aaaaaaaaaaaaaa.....[2万文字とか、ヘッダーの総文字数が16384文字を超える]
CookieやUser-Agentに限らず、HTTPリクエストヘッダの総文字数が、サーバー側で許容する文字数以上に長いものだと、400エラーが発生します。
IISの場合は、バイト数で16384文字(16k)がデフォルト値になっているので、これを超えるとエラーします。
このリクエストを作るコード例は、以下。
$cookieValue = "".PadLeft(19999, "a") $request = @" GET / HTTP/1.1 HOST: 172.24.0.31 Cookie: a=$cookieValue "@ $request += "`r`n`r`n"
Cookie値 a が 19999文字の"a"が入ったリクエストを作る。サーバー側ではHTTPヘッダが大きすぎて拒否され、400エラーになります。
この時のレスポンスは、HOSTヘッダー無しでリクエストした結果と同様に、Serverヘッダ値が Microsoft-HTTPAPI/2.0 と表示されている。
リクエストライン (Request Line) が不正だと、400エラー
リクエストの1行目は、Request Line と言い、正しいフォーマットにする必要があります。
"[HTTP動詞] [要求URL] [HTTPバージョン]" と、空白区切りの3要素が必要になるところですが、このフォーマットに沿っていない場合に、400エラーが発生します。
リクエストラインが、
GET /
や
GET HTTP/1.1
その他
a
などは、全てエラーします。
これらのリクエストは、RFCに違反しているリクエストで、ブラウザは作らないはずなので、このリクエストを受けたサーバー側は直接的な攻撃をされているとみなされるものでしょう。
SSLアクセスでの確認
上記、これまで http:80番ポートで平文のアクセスで確認しましたが、実際の稼働サイトが https:443番ポートで動いているところでも確認しました。
SSL/TLSのハンドシェイクを自前で行うのは大変なので、.NET Framework の WebRequestとWebResponseを使います。
PowerShellからのおおまかなコードはWebリクエストの記事 PowerShellでWebリクエスト にあります。
これまで確認したものと同じ Windows Server 2016 の IIS (IIS 10) のWebサーバーにアクセスしますが、https://172.24.0.31/ ではFQDNが相違してアクセスできない環境なので、外部からアクセスできるホスト名でアクセスします。このため、以下、便宜上ホスト名を hogehoge.net と仮定した記述にしています。
まず、ルート (/) への正常なリクエストの記述です。
$webRequest = [System.Net.WebRequest]::Create("https://hogehoge.net/")
または、
$webRequest = [System.Net.WebRequest]::Create("https://172.24.0.31/") $webRequest.Host = "hogehoge.net"
どちらの書き方でもアクセスできます。Createの引数のFQDN部分は、自動的にHostプロパティになるので、IPアドレスのURLでCreateした場合には、Hostプロパティを明示する必要があります。
この時のレスポンスは 200 OK で内容は下記になります。
HTTP/1.1 200 OK Accept-Ranges: bytes Content-Length: 703 Content-Type: text/html Date: Wed, 30 Dec 2020 03:32:54 GMT ETag: "b56cb1c9587d61:0" Last-Modified: Tue, 31 Mar 2020 12:34:57 GMT Server: Microsoft-IIS/10.0 X-Powered-By: ASP.NET
では、400エラーを発生させるための %25 を付けたリクエストをしてみます。
$webRequest = [System.Net.WebRequest]::Create("https://hogehoge.net/%25")
レスポンス結果
HTTP/1.1 400 Bad Request Content-Length: 33 Cache-Control: private Content-Type: text/html Date: Wed, 30 Dec 2020 03:44:14 GMT Server: Microsoft-IIS/10.0 X-AspNet-Version: 4.0.30319 X-Powered-By: ASP.NET 要求が正しくありません
http:80 の平文のリクエストで行ったものと同じ応答が返ってきました。
次に、Cookieに大きな値を設定して、HTTPリクエストが大きくなるようにしてみます。
$webRequest = [System.Net.WebRequest]::Create("https://hogehoge.net/") $cookieValue = "".PadLeft(19999, "a") $webRequest.Headers["Cookie"] = "a=$cookieValue"
レスポンス結果
HTTP/1.1 400 Bad Request Connection: close Content-Length: 346 Content-Type: text/html; charset=us-ascii Date: Wed, 30 Dec 2020 03:51:04 GMT Server: Microsoft-HTTPAPI/2.0
これも http:80 の平文のリクエストで行ったものと同じ応答が返ってきました。
System.Net.WebRequestでは、リクエストライン (Request Line) を不正にすることや、Hostプロパティを空にしてHOSTヘッダーを無くすリクエストは残念ながら作れないので、それらの確認はできませんでしたが、TLSハンドシェイクでの暗号/複合の後のリクエストからの挙動は平文のリクエストと同じになるはずです。
400エラーを発生させる方法 まとめ
400エラーが発生するリクエストは、大体こんなものでしょう。
- URLパスの末尾に % がある
- HOSTヘッダー値が無い
- HTTPリクエストヘッダが大きすぎる
- リクエストライン (Request Line) が不正
この検証結果から、ブラウザからのアクセスで、400エラーが発生した時に、Cookieを削除して対応するということも合っています。
Webアプリケーションで複数のCookieを大量にセットすれば、リクエスト時のHTTPヘッダも大きくなってくるので、400エラーが発生するということでしょう。
通常ならあまり起こらないと思いますが、おそらく常時使っていないブラウザで数カ月ぶりにアクセスしたとか、古いCookieが削除されずに残っている場合などで発生しやすいのかなと思います。
このような場合、Webアプリケーションが古いCookieを削除する対応をしていないということも原因にもなるので、Webアプリケーション側で経年を通してトークンの違うCookieを乱発しないとか心掛けていれば発生しないと思います。
コメントする