はじめに
Webセキュリティに関しても備忘録まとめていく。
2018年に購入していたにも関わらず、途中までで読みきれていなかった「安全なWebアプリケーションの作り方」より。
脆弱性が生まれる理由
主に2種類
1)バグによるもの
- SQLインジェクション
- XSS(クロスサイト・スクリプティング)
2)チェック機能不足
- ディレクトリ・トラバーサル
HTTPとセッション管理
HTTP通信はクライアント(通常はウェブブラウザ)とサーバー間のリクエストとレスポンスのプロセスで構成される。ステートレス(サーバー側では状態を保持しない)
ウェブブラウザがウェブページをリクエストすると、サーバーは適切なレスポンスを返す。
GETメソッド
リクエストメッセージ
# リクエストライン
GET / HTTP/1.1
# ヘッダ-
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Connection: keep-alive
Host: www.example.com
リクエストライン:リクエストメソッド(GET)・パス・プロトコルバージョンからなる。
- リクエストメソッド: この例では
GET
。サーバーに特定のリソースを要求する。 - パス(URL):
/
。これはルートディレクトリを指し、ウェブサイトのホームページを要求している。通常はホスト名(今回だとwww.example.com)を含まない - プロトコルバージョン:
HTTP/1.1
。
ヘッダー
- リクエストメッセージの2行目以降。名前と値をコロン「:」で区切った形
- リクエストに関する追加情報。
Host
はリクエストが送られるサーバーのドメインを示し、User-Agent
はリクエストを行っているクライアントの情報を提供する。 - 必須なのはHostだけ。
HTTPレスポンス
HTTP/1.1 200 OK
Date: Mon, 23 May 2024 22:38:34 GMT
Server: Apache/2.4.1 (Unix)
Content-Type: text/html; charset=UTF-8
Content-Length: 155
Last-Modified: Wed, 20 May 2024 22:11:15 GMT
Connection: close
<html>
<head>
<title>An Example Page</title>
</head>
<body>
Hello World, this is a very simple HTML document.
</body>
</html>
ステータスライン
HTTP/1.1 200 OK
。これはプロトコルバージョン、ステータスコード(200)、ステータスメッセージ(OK)を示す。200はリクエストが成功したことを意味する。
レスポンスヘッダー:
- レスポンスメッセージの2行目以降
- サーバーに関する情報とレスポンスのメタデータ。
Content-Type
はレスポンスのMIMEタイプを示し、Content-Length
はレスポンスのコンテンツのサイズをバイトで示す。 - MIME(Multipurpose Internet Mail Extensions)タイプは、データの種類や形式を識別するための標準化された識別子。MIMEタイプは通常、データの種類とサブタイプの組み合わせで表される。テキスト文書のMIMEタイプは
text/plain
であり、JPEG画像のMIMEタイプはimage/jpeg
ボディ
- 実際のコンテンツ。この例ではHTMLドキュメント
POSTリクエスト:Referer / パーセントエンコーディング
- クライアントからサーバーへデータを送信するために使用される。
- 通常、フォームの送信、ファイルのアップロード、またはウェブアプリケーションに対する命令を送る際に使われる
例)ユーザーの名前が「田中 太郎」で年齢が「30」、そして送信ボタンをクリックした場合
POST /submit-form HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 58
Referer: http://www.example.com/form.html
name=%E7%94%B0%E4%B8%AD%20%E5%A4%AA%E9%83%8E&age=30&submit=Submit
リクエストライン
POST /submit-form HTTP/1.1
は、/submit-form
パスにPOSTメソッドを使用し、HTTPバージョン1.1プロトコルを通じてリソースを要求することを示す。
ヘッダー
Content-Type: application/x-www-form-urlencoded
は、フォームデータがURLエンコードされていることを意味する。Content-Length
はボディの長さを示しす。
Referer
http://www.example.com/form.html
は、リクエストが発生した元のページ(リファラー)を示す。これはサーバーがどのページからリクエストが来たかを知るのに役立つ。- refererがセキュリティ上の問題になるのは、URLが秘密情報を含んでいる場合。典型的にはURLがセッションIDを含んでいる場合、Referer経由で外部に漏洩して、成りすましに悪用される可能性がある。
パーセントエンコーディング:
- URL内で使用できない文字や、特別な意味を持つ文字をエンコードするためのメカニズム
- メッセージボディの中で、
田中 太郎
は%E7%94%B0%E4%B8%AD%20%E5%A4%AA%E9%83%8E
にパーセントエンコーディングされている。ここで「田」は%E7%94%B0
、「中」は%E4%B8%AD
、「太」は%E5%A4%AA
、「郎」は%E9%83%8E
に、そしてスペースは%20
に変換されている。これにより、日本語や他の特殊文字が含まれるデータでも、HTTPリクエストを介して正確に送信することが可能になる。
GETとPOSTの使い分け
- データ更新など副作用を伴うリクエストの場合→POST
- 秘密情報を送信する場合→POST(GETの場合、URL上に指定されたパラメーターが流出する危険があるため)
- 送信するデータの総量が多い場合→ORT
- 参照(リソースの取得)のみ→GET
hiddenパラメーター
利用者自身により書き換えできる。情報漏洩や第三者からの書き換えに対しては堅牢
HTMLフォームのhidden
パラメーターは、クライアントサイドで書き換え可能。これらのパラメーターは、フォームが表示されているページのHTMLソースに存在するため、ユーザーがブラウザの開発者ツールを使用すると見たり、変更したりできる。
<form action="submit.php" method="post">
<input type="hidden" name="user_id" value="12345">
<input type="text" name="comments">
<input type="submit" value="Submit">
</form>
認証と認可の違い
認証(authentication):利用者が確かに本人であることを何らかの手段で確認すること
認可(authorization):認証ずみの利用者に権限を与えること。具体的には、データの参照・更新・削除や物品の購入など
Basic認証
HTTPプロトコルで定義された、最もシンプルな認証の形式の1つ。ステートレス
ユーザー名とパスワードを組み合わせてサーバーに送信することで、ユーザーの身元を確認する方法
具体的な動作の流れ
- ユーザーが保護されたウェブリソースにアクセスしようとする。
- サーバーは認証が必要であることを示すレスポンス(ステータスコード 401 Unauthorized)をブラウザに送信し、
WWW-Authenticate
ヘッダーにBasic
認証を要求する。 - ブラウザはユーザーにユーザー名とパスワードの入力を求めるダイアログボックスを表示する。
- ユーザーが認証情報を入力すると、ブラウザはその情報を
username:password
の形式で結合し、この文字列をBase64エンコードして、再度サーバーにリクエストを送信する。このエンコードされた文字列は、Authorization
ヘッダーにBasic
の後に続けて設定される。
例えば、ユーザー名が user
、パスワードが pass
の場合、これらを user:pass
という形に結合し、Base64でエンコードする。Base64エンコードされた user:pass
は dXNlcjpwYXNz
になる。
したがって、HTTPリクエストのヘッダーには以下のように記載される:
Authorization: Basic dXNlcjpwYXNz
Base64エンコードは、情報を隠すためのものではなく、バイナリデータ(ここではuser:password
という文字列はバイナリデータとして扱われ(内部的には文字列もバイナリデータとしてコンピュータに保存されている)、Base64エンコードによって安全に転送できるテキスト形式に変換される)をテキストフォーマットにエンコードするためのもの。そのため、誰でもこの文字列をデコードして元のユーザー名とパスワードを取得することが可能。Base64エンコードされた dXNlcjpwYXNz
をデコードすると、user:pass
に戻る。
このため、Basic認証はHTTP通信が暗号化されていない場合、中間者攻撃によってユーザー名とパスワードが盗まれる危険性がある。そのため、通常はHTTPSと組み合わせて使用し、通信内容を暗号化することが推奨される。
セッション管理
HTTP認証(Basic認証やDigest認証)を使えば、ブラウザ側でIDとパスワードを記憶してくれる(ブラウザがユーザーの認証情報をメモリに一時的に保持するため)が、HTTP認証を使わない場合は、サーバー側で認証状態を覚えておく必要がある
→セッション管理という概念が登場
ブラウザが認証情報を記憶するのは通常、そのブラウザウィンドウが開いている間、つまりセッション中のみ。ブラウザを完全に閉じて新しいセッションを開始すると、再度認証情報の入力が求められる。
クッキー(cookie)
- サーバー側からブラウザ側に対して「名前=変数」の組を覚えておくように指示するもの。ブラウザはクッキーデータをそのユーザーのデバイス上の特定の場所にファイルとして保存する(HTTP認証のみだと、ブラウザ上にしかユーザー認証情報は保存されないので、ユーザーがブラウザを閉じると認証情報も消去される)
例えばGoogle ChromeブラウザをmacOSで使用している場合、クッキーデータは以下の場所に保存される。
~/Library/ApplicationSupport/Google/Chrome/Default/Cookies
ただし、これらのファイルは通常のテキストエディタでは読むことができず、クッキーを閲覧・管理するにはブラウザの設定メニューや専用のツールを使用する必要がある。
- セッション管理をHTTPで実現する目的で導入された。
- クッキーは主に、ユーザーのセッション情報や設定、ログイン状態などを記録し、ユーザーがWebサイトを訪れるたびにこれらの情報をサーバーに送り返すことで、パーソナライズされた体験やセッションの維持を可能にする。
- クッキーはブラウザごとに管理される。つまり、同じコンピュータで異なるブラウザを使用している場合、各ブラウザはそれぞれ独立したクッキーを保持している。
ログイン時のHTTPレスポンス例:
ユーザーがIDとパスワードを入力してログインボタンをクリックすると、サーバーは以下のようなHTTPレスポンスを返す。
HTTP/1.1 200 OK
Content-Type: text/html
<span style="background-color: rgb(251, 243, 219);">Set-Cookie: session_id=abc123; Path=/; HttpOnly</span>
このレスポンスに含まれるSet-Cookie
ヘッダーは、ブラウザにsession_id
という名前のクッキーを設定するように指示している。このクッキーにはabc123
という値が格納され、これによりユーザーセッションが一意に識別される。Path=/
はこのクッキーがウェブサイトのすべてのページで有効であることを示し、HttpOnly
属性はクッキーがJavaScriptからアクセスできないようにするためのもの。
再訪問時のHTTPリクエスト例:
ユーザーがログイン後、または別の日に同じサイトを訪れると、ブラウザは以下のようなHTTPリクエストをサーバーに送信する。
GET /index.html HTTP/1.1
Host: www.example.com
<span style="background-color: rgb(251, 243, 219);"><strong>Cookie: session_id=abc123</strong></span>
このリクエストに含まれるCookie
ヘッダーは、前回の訪問時に設定されたクッキーをサーバーに送り返している。サーバーはこのsession_id
クッキーを使用して、ユーザーのセッションを識別し、ユーザーが以前に行った操作や設定を復元することができる。これにより、ユーザーは再度ログインすることなく、サイトをパーソナライズされた形で利用できるようになる。
クッキーの属性:Domain, Secure, HttpOnly
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
クッキーを発行する際には、様々なオプションの属性を設定できる。
セキュリティ上重要な属性は、Domain, Secure, HttpOnlyの3つ
Domain属性:デフォルトON
- 異なるドメインに対するクッキーが設定できると、セッションIDの固定化攻撃の手段として利用されるので、通常は異なるドメインに対するクッキー設定はしない。
- デフォルトもOFF(つまり、現在のドキュメントのドメインにのみ関連付けられる)になっているので、そのままにしておく
Secure属性:デフォルトOFF
- Secure属性をつけたクッキーは、HTTP通信の場合のみサーバーに送信される。基本的につける
- この属性が設定されていない場合、クッキーはHTTPおよびHTTPSの両方のリクエストで送信される
HttpOnly属性:デフォルトOFF
- XSS(クロスサイト・スクリプティング)攻撃により、JavaScriptを悪用してクッキーを盗み出すリスクを減らすために、基本的につける
その他
Path属性
- この属性が設定されていない場合、クッキーはクッキーが設定されたウェブページのパスとサブパスに対して有効になる。パスを指定すると、そのパス以下のページでのみクッキーが利用可能になる。
Expires属性
- この属性が設定されていない場合、クッキーは「セッションクッキー」となり、ブラウザが閉じられると削除される。
Expires
またはMax-Age
を設定することで、クッキーに有効期限を設けることができる。 Expires
属性を使用してクッキーの有効期限を設定する場合、特定の日時をGMT(グリニッジ標準時)で指定する指定する必要がある。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure
- Expires属性を指定しない場合、クッキーはブラウザが閉じられると削除されるので、ユーザーの視点から見た場合(ユーザー体験)には、HTTP認証のみのブラウザでの認証保存と変わらない。
受動的攻撃と同一オリジンポリシー
同一オリジンポリシー(same origin policy)
- ブラウザのサンドボックスに用意された制限の一つ
- JavaScriptなどのクライアントスクリプトからサイトを跨ったアクセスを禁止する
攻撃のシナリオとして、サイトA(罠サイト)にユーザーを誘導し、そこに埋め込まれた<iframe>
を介してサイトB(例えば、ユーザーがログインしている銀行のウェブサイトなど)の情報を読み取ろうとするケースを考えてみる
具体的な攻撃シナリオ:
ユーザーを罠サイトに誘導する:
- 攻撃者はユーザーを罠サイト(サイトA)に誘導する。フィッシングメール、ソーシャルメディアを通じたリンク共有、あるいは検索エンジン最適化(SEO)の悪用などの手法を使用。
<iframe>
を利用した攻撃:
- 罠サイトには、サイトBのコンテンツを表示する
<iframe>
が含まれている。攻撃者は、JavaScriptを使用してこの<iframe>
内のデータにアクセスしようと試みる。
サイトAのHTMLコードの例:
<html>
<body>
<h1>罠サイトにようこそ!</h1>
<iframe src="http://siteB.com" id="targetFrame"></iframe>
<script>
// JavaScriptによる攻撃コード
var iframe = document.getElementById('targetFrame');
try {
var innerDoc = iframe.contentDocument || iframe.contentWindow.document;
// ここでiframe内のデータにアクセスしようと試みる
console.log(innerDoc.body.innerHTML);
} catch (e) {
console.error('アクセスできません: ', e);
}
</script>
</body>
</html>
しかし、同一オリジンポリシーにより、異なるオリジン(サイトAとサイトB)間でのこのようなアクセスはブロックされ、攻撃は失敗する。
ただし、XSSはJavaScriptを送り込むことで、同一オリジンポリシー下でJavaScriptを実行し、コンテンツを盗み出すことができてしまう。
同一オリジンポリシーにおいて、「オリジン」はスキーム(プロトコル)、ホスト(ドメイン名)、ポート番号の3つがすべて一致する場合に、2つのリソースは「同一オリジン」とみなされる。
例1:同一オリジンの場合
- リソース1:
http://example.com:80/page1.html
- リソース2:
http://example.com:80/page2.html
これらはプロトコル(http)、ホスト(example.com)、ポート番号(80)がすべて一致しているため、同一オリジン。
例2:ホストが異なる場合(異なるオリジン)
- リソース1:
http://example.com/page1.html
- リソース2:
http://sub.example.com/page2.html
サブドメインもホストの一部として扱われるので、example.com
とsub.example.com
は異なるホストのため、異なるオリジンとみなされる。
例3:プロトコルが異なる場合(異なるオリジン)
- リソース1:
http://example.com/page1.html
- リソース2:
https://example.com/page2.html
プロトコルが一つはhttpで、もう一つはhttpsなので、これらは異なるオリジン。
例4:ポート番号が異なる場合(異なるオリジン)
- リソース1:
http://example.com:80/page1.html
- リソース2:
http://example.com:8080/page2.html
ポート番号が一つは80で、もう一つは8080なので、これらは異なるオリジン。
CORS(Cross-Origin Resource Sharing)
ウェブページが別のオリジン(ドメイン、スキーム、ポート)からリソースをリクエストできるようにするためのメカニズム。
通常、ブラウザの同一オリジンポリシーは異なるオリジン間でのリソース共有を制限するが、CORSは特定の条件下でこれを緩和する。
CORSで中心的な役割を果たすのがAccess-Control-Allow-Origin
レスポンスヘッダー。このヘッダーは、サーバーからクライアントに送られ、特定のオリジンがリソースにアクセスすることを許可するかどうかをブラウザに指示する。
Access-Control-Allow-Originの動作
- 特定のオリジンからのアクセスを許可する場合:
サーバーは
Access-Control-Allow-Origin
ヘッダーに許可するオリジンを明示的に指定する。例えば、Access-Control-Allow-Origin: http://example.com
はhttp://example.com
からのリクエストを許可する。 - すべてのオリジンからのアクセスを許可する場合:
サーバーは
Access-Control-Allow-Origin
ヘッダーにワイルドカード*
を使用して、どのオリジンからのリクエストも許可することができる。例えば、Access-Control-Allow-Origin: *
はすべてのオリジンからのリクエストを許可する。
例)クライアント(http://example.comのウェブページ)が異なるオリジン(http://api.example.com)にあるAPIからデータを取得しようとするシナリオ
HTTPリクエスト
Origin
ヘッダー:クロスオリジンHTTPリクエストを行う際にブラウザによって自動的に設定されるヘッダー
GET /some-data HTTP/1.1
Host: api.example.com
Origin: http://example.com
サーバー(api.example.com)は次のようなレスポンスを返す。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://example.com
Content-Type: application/json
{"key":"value"}
このレスポンスでは、Access-Control-Allow-Origin: http://example.com
ヘッダーにより、http://example.com
からのアクセスを許可している。これにより、example.comのページからapi.example.comへのAPIリクエストが可能になる。
クッキーなど認証用のヘッダを伴うクロスオリジンリクエスト:追加設定すべき2項目
withCredentials プロパティ
withCredentials
プロパティは、クロスオリジンリクエストでクッキーや認証ヘッダー(例えば、HTTP認証やOAuthトークン)、TLSクライアント証明書などの認証情報を使用するかどうかを制御するために使用される。
クロスオリジンリクエストでは、これらの認証情報はデフォルトではセキュリティ上の理由から送信されない。具体的にはCSRF攻撃の防止、ユーザーのセッション情報の漏えいの防止など。
しかし、特定の場合には、クロスオリジンリクエストに認証情報を含める必要がある。たとえば、ユーザーがログインした状態で異なるオリジンのAPIからデータを安全に取得する場合など。このような場合には、XMLHttpRequestやFetch APIのwithCredentials
プロパティをtrue
に設定する。
XMLHttpRequest
(XHR)
- JavaScriptを使用してHTTPを介してサーバーと非同期にデータを交換するためのWeb API
- このオブジェクトを使用すると、Webページが全体を再読み込みすることなく、バックグラウンドでサーバーとのデータ交換が可能になる。これにより、ページの一部分のみを更新する動的なWebアプリケーションを作成できる
XMLHttpRequestの例:
var xhr = new XMLHttpRequest();
xhr.open('GET', '<http://example.com/data>', true);
xhr.withCredentials = true;
xhr.send(null);
Fetch APIの例:
fetch('<http://example.com/data>', {
method: 'GET',
credentials: 'include'
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
レスポンスヘッダー:Access-Control-Allow-Credentials: true
サーバー側では、クロスオリジンリクエストに認証情報を含めることを許可するために、適切なCORSヘッダーをレスポンスに含める必要がある。具体的には、Access-Control-Allow-Credentials
ヘッダーを true
に設定する。
Access-Control-Allow-Credentials: true
このヘッダーが設定されていない場合、ブラウザは認証情報を含むクロスオリジンリクエストをブロックする。また、Access-Control-Allow-Credentials
が true
の場合、Access-Control-Allow-Origin
ヘッダーはワイルドカード(*
)を使用できない。具体的なオリジンを指定する必要がある。
Access-Control-Allow-Origin: <http://yourwebsite.com>
これらの設定により、クロスオリジンリクエストでセキュアに認証情報をやりとりできるようになる。
コメント