GETメソッドとPOSTメソッドの違いを実感する実験

8 min読了の目安(約7200字TECH技術記事

なぜ実験をしたのか

後輩に聞かれてテンパったのでおさらいしてみました。確実な正解を知りたい人はRFC 7231に記載されていますので読んでください。それが一番です。以下に続くのは、私の実験と戯言です。

シンプルに言えばGETは参照に使い、POSTは更新や機密情報送信に使うといったイメージですが、やろうと思えば真逆の実装もできるでしょう。その代わりこれはマズいことになります。HTTPはアプリだけでなくブラウザもミドルウェアも絡んでいて、それぞれがルールを意識して作られているからです。みんな(ブラウザやミドルウェアの中の人たち)がどのようにそれぞれのメソッドを区別しているかを実験して具体的に確かめてみました。

実験

実験1 パラメータの取り扱いの差

GETでは基本的にパラメータをクエリ文字列に含みます。一方POSTではパラメータをボディに含んだリクエストを使えます。あるページからパラメータを伴って他のページへ遷移する時、一般的には以下のようなHTMLが使われます。

GETの場合

<html><body>
  <a href="/?queryparam=123">Lets go!</a>
</body></html>

POSTの場合

<html><body>
<form action="/" method="POST">
  <input name="bodyparam" value="123">
  <input type="submit" value="Lets go!">
</form>
</body></html>

送信されるリクエストは以下のようになります。(主要部分のみ抜粋)

GETの場合

GET /?queryparam=123 HTTP/1.1
Host: example.com

POSTの場合

POST / HTTP/1.1
Host: example.com

bodyparam=123

そしてクエリ文字列に含まれたパラメータと、ボディに含まれたパラメータでは扱いが異なります。ざっくりいうと前者は扱いが軽く、後者は秘密情報を守るようになっています。例えばWebサーバのログがそうです。リクエストに含まれるパラメータが、それぞれどのようにログに残されるかを確認してみました。

nginxではアクセスログに以下のような出力がありました。

10.30.1.1 - - [03/Dec/2020:10:39:26 +0000] "GET /?queryparam=123 HTTP/1.1" 200 612 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0" "-"
10.30.1.1 - - [03/Dec/2020:10:39:26 +0000] "POST / HTTP/1.1" 405 157 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0" "-"

続いてApacheではこちら。

10.30.1.1 - - [06/Dec/2020:19:37:54 +0900] "GET /?queryparam=123 HTTP/1.1" 200 552 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0"
10.30.1.1 - - [06/Dec/2020:19:37:54 +0900] "POST / HTTP/1.1" 200 552 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0"

IISではこうです。

2020-12-25 08:48:40 127.0.0.1 GET / queryparam=123 443 - 127.0.0.1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/87.0.4280.88+Safari/537.36 https://getpost.example.com/logcheck.html 200 0 0 1346
2020-12-25 08:48:43 127.0.0.1 POST / - 443 - 127.0.0.1 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/87.0.4280.88+Safari/537.36 https://getpost.example.com/logcheck.html 200 0 0 3

見ての通り、queryparamはバッチリ残っていますが、bodyparamは影も形もありません。つまり、GETのパラメータにパスワードや個人情報を含むと、デフォルトでは実体のあるログファイルとして残るということですね。Webサイトの運用者がログを確認するとこれが見られてしまいます。またサーバに脆弱性があり、外部からローカルファイルが閲覧されてしまう場合は、攻撃者が機密情報を得る大きな助けとなるでしょう。POSTはボディにパラメータを持たせることができるので、Webアプリで意図して出力しなければこのような問題は起こらないはずです。

ところで、こういったHTMLだとどうなるのでしょうか。これは良く見かけるものです。

<html><body>
<form action="/?postqp=abc" method="POST">
  <input name="postbp" value="123">
  <input type="submit" value="Lets go!">
</form>
</body></html>

送られたHTTPリクエストです。

POST /?postqp=abc HTTP/1.1
Host: example.com

bodyparam=123

クエリ文字列にもボディにもパラメータがあります。クエリ文字列の部分はログに残ります。以下はApacheのログです。

10.30.1.1 - - [25/Dec/2020:09:43:42 +0900] "POST /?postqp=abc HTTP/1.1" 200 552 "http://10.30.1.1/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"

クエリ文字列のパラメータはログに残っています。POSTだからとかGETだからというわけではないのですね。
続いてformタグのmethod属性をGETにしたらどうなるのでしょう。これはあまり見かけません。というか私は見たことがありません。

<html><body>
<form action="/?getqp=abc" method="GET">
  <input name="getbp" value="123">
  <input type="submit" value="Lets go!">
</form>
</body></html>

まさかGETでもPOSTのようにボディにパラメータを持たせて送ってくれるのでしょうか。そんなことありませんでした。このHTMLを開いてボタンを押したとき、ブラウザはこのようなリクエストを送ってくれました。

GET /?getbp=123 HTTP/1.1
Host: 10.33.2.76

クエリ文字列のgetqp=abcは消え去り、getbp=123で上書きされました。ブラウザ(Chrome)はやはりGETのボディとしては送ってくれないようです。ログに残るのも当然後者です。

10.30.1.1 - - [25/Dec/2020:09:46:57 +0900] "GET /?getbp=123 HTTP/1.1" 200 552 "http://10.30.1.1/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"

なぜこうなのかというと、MDNにちゃんと書いてました[1]

get: HTTP GET メソッドに相当します。フォームのデータは '?' をセパレーターとして action 属性の URI に追加して、その結果となる URI をサーバにー送信します。

ブラウザはこの通りに動いてるだけですね。なおmethodが無い場合のデフォルトはGETとなります。

ここまでの結果から以下のことが言えます。

パラメータの取り扱いの違いから、機密情報の送信にはGETよりPOSTが適していますが、リクエストのボディ部分で送られるようにしないといけません。

実験2 Cookieの取り扱いの差

HTTPのCookieにはSameSiteという属性があります。これはサードパーティからのリクエストについてCookieを付けて送るか付けずに送るかを指定できます。この「サードパーティ」というのがとても分かりにくいのですが、クロスオリジンのように異なる「Site[2]」からのリクエストということです。「Site」は我々が普段口にする「サイト」とは若干意味が異なることに注意してください。詳しくは脚注のリンク先を参照してください。
そしてSameSiteという属性にはNone,Lax,Strictという3種類の値があり、LaxはGETかPOSTによって挙動が変わります。
MDN[3]では

サードパーティの Web サイトによって開始された GET リクエストとともに送信されます。

と記載されています。はっきりGETと書かれていますので、GETかPOSTかで扱いが異なるということですね。
今回はSameSite属性がLaxであるものとStrictであるもの2種類のCookieをセットし、サーバ側に送られるか試してみました。
ヘッダの情報を見るために、サーバ側では次のようなPHPを動作させておきます。これはリクエストヘッダに含まれていたCookieを表示するだけのシンプルなものです。

testphp.php
<body>
<pre>
<?php
echo "メソッド-> " . $_SERVER["REQUEST_METHOD"] . "\n";
echo "サーバで受け取ったCookie" . "\n";
print_r($_COOKIE);
?>
</pre>
</body>    

クライアント側でCookieをセットしておきます。方法はいろいろありますが、今回は開発者ツールを使いました。

2つのCookieがセットされており、cookieTestLaxはSameSite属性の値がLaxに、cookieTestStrictはStrictに、名前の通り設定されていることが分かると思います。この状態で、サードパーティとなるドメインのHTMLファイルからWebサーバへリクエストを送信してみましょう。

送信元:src.example.jp

送信先:getpost.example.com/testcook.php

TLDが異なることに注意してください。getpost.example.jpにしてしまった場合、オリジンとしては異なりますが、Cookieとしては異なるSiteとして見做されません。

ちなみにこの実験のためにサーバを2台作る必要はありません。hostsを編集して2種類のドメインに同一IPを設定するだけです。

GETの場合、結果はこちらです。

SameSite属性の値がLaxのものだけが送信されました。

POSTの場合、結果はこのようになりました。

Laxのものも送られていません。

SameSite属性の値がLaxであるとき、Cookieの扱いはGETかPOSTで異なりますね。またブラウザによってはSameSite属性が指定されてない場合はLaxとして扱われます。

ちなみにSameSite属性によって送られなかったCookieは「送られなかった」だけであり、JavaScriptからは(HttpOnly属性が無ければ)普通にアクセスできます。シンプルに見たいのでPHPで作ったページのHTML部分にScriptタグを追加します。

<script>document.write("JavaScriptから見えるCookie<br>" + document.cookie);</script>

GET

POST

Strictも出てきていますね。

その他:クローラの挙動

本当はこれも実験したかったのですが、自サイトをGoogleがなかなかクロールしてくれないので諦めました。
GooglebotのようなクローラはGETとPOSTを区別します。クローラはPOSTメソッドでのリクエストを自動で送ることは好ましくないとされています[4]。データの削除が発生する箇所がGETで機能するよう実装されると、クローリングで全滅なんてことになりかねません。こういうのは危険です。ただGooglebotでは条件付きでPOSTリクエストも送るそうです[5]が、GETのように無条件に送る事は無いようです。

データベースへの書き込みやメールの送信など、クロールされるとマズいことが発生する機能にはPOSTを使いましょう。

結論

ブラウザやサーバがGETかPOSTによって取り扱いを変えている様子がわかっていただけたかと思います。こういったことを気にせずアプリを作ることも、できなくはないと思いますがトラブルの元になるだけでしょう。決まり事は守っていきましょう。

脚注
  1. <form> - HTML: HyperText Markup Language | MDN
    https://developer.mozilla.org/ja/docs/Web/HTML/Element/form ↩︎

  2. Site (サイト) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
    https://developer.mozilla.org/ja/docs/Glossary/Site ↩︎

  3. SameSite cookies - HTTP | MDN
    https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Set-Cookie/SameSite ↩︎

  4. クローリングハック あらゆるWebサイトをクロールするための実践テクニック
    竹添 直樹, 島本 多可子, 田所 駿佑, 萩野 貴拓, 川上 桃子
    翔泳社 https://www.shoeisha.co.jp/book/detail/9784798150512 ↩︎

  5. Google ウェブマスター向け公式ブログ [JA] : より多くの有益なコンテンツを検索結果に: クローラが POST リクエストにも対応しました
    https://webmaster-ja.googleblog.com/2012/01/post.html ↩︎