Webアプリケーションは、不特定多数のアクセスが見込まれることが多いプログラムです。そのため、セキュリティにはネイティブアプリケーション以上の注意が必要となります。
ディレクトリトラバーサル
近年のRESTfulなWebアプリケーションでは、クエリパラメータやリクエストBodyで渡されたパラメータに応じて処理を行うケースが多くあります。
もし、Webアプリケーションが、サーバーの静的なファイルを読み取って返すような動作を行う場合は、この攻撃に配慮しなければならないかもしれません。
攻撃手法
例えば、ログファイルを直接指定して画面に表示するようなプログラムを作るとします。クライアントから受け取るパラメータはファイル名とします。
受け取るファイル名の想定
apache.log
パスの組立処理
String LOG_HOME = "/var/log/";
String fileName = "{リクエストされたファイル名}";
String fullPath = LOG_HOME + fileName;
結果
/var/log/apache.log
こうなるはずです。しかし、攻撃者はログファイルではなく、攻撃の取っ掛かりとなるようなファイル、たとえば、/etc/passwd
や、アプリケーションの設定ファイルが欲しいわけです。
つまり、以下のようなパラメータを渡してきます。
../../../etc/passwd
結果、こうなります。
/var/log/../../../etc/passwd
まんまと目的のファイルが表示されてしまいました。
対策
ファイル名に入力値を直接使わない
ファイル名の一部をサーバー側で付与することによって、先ほどの試みはいとも簡単に崩れます。例えば、以下のようにします。
String LOG_HOME = "/var/log/";
String fileName = "{リクエストされたファイル名}";
String fullPath = LOG_HOME + fileName + ".log";
これなら、攻撃者がpasswdファイルを盗み見ようと試みても・・・
/var/log/../../../etc/passwd.log
No such file or directory
となります。ただしこれは、別の見せたくないディレクトリにあるログファイルを閲覧されてしまう可能性があります。
IDを受け取り、サーバー側の設定値に変換する
安全な方法です。見せたいファイルとIDを紐づければ、IDに登録されているファイル以外を閲覧する術がありません。更に、ここにフルパスを指定しておけば、ディレクトリに囚われることなくどんなファイルも閲覧可能です。
int id = "{リクエストされたID}";
// たとえば、HashMapオブジェクトにIDとフルパスが紐づけられている場合
String fullPath = map.get(id);
実際の運用では、ファイル名に変更が見込まれるファイルはDBに格納したりするのがよいでしょう。
有害な文字を除去する
../
や..
などの、攻撃に使われる文字をあらかじめ除去する方法です。
デコード処理さえ気を付ければ問題なく運用可能なはずですが、十分なデバッグを行う必要があります。
SQLインジェクション
有名な攻撃です。SQL文を組み立てる際に、適切な処理を行っていない場合に発生する可能性があります。
攻撃手法
とあるログイン処理を組み立ててみます。IDとパスワードが合致していればログイン成功とする単純極まりないものです。
public boolean tryLogin(String userId, String password){
String sql = "SELECT * FROM user WHERE user_id = '" + userId + "' AND password = '" + password + "'";
Statement statement = connection.createStatement(sql);
ResultSet result = statement.executeQuery();
if(!result.first()){
// ログイン失敗
return false;
}
// ログイン成功
return true;
}
このコードには問題があります。ユーザーが普通にIDとパスワードを入力してくれればいいのですが・・・。
渡されるパラメータ
ユーザーID | パスワード |
---|---|
root | password123 |
組み立てられるSQL文
```sql
SELECT * FROM user WHERE user_id = 'root' AND password = 'password123'
このSQL文なら、問題なくログイン処理が行えます。しかし、攻撃者は次のようなパラメータをセットしてきます。
渡されるパラメータ
ユーザーID | パスワード |
---|---|
a | ‘ OR ‘A’ = ‘A |
組み立てられるSQL文
SELECT * FROM user WHERE user_id = 'a' AND password = '' OR 'A' = 'A'
こうすると、ユーザーIDとパスワードがどんな値であっても、その後のOR句がすべてtrueとなるため、全てのレコードが返されるようになります。例のコードでは、レコードが一つでもあればログイン成功とみなされるので、攻撃者は機密情報にアクセスすることができてしまいます。
対策
プリペアドステートメントを使う
これ一本でOKです。確実かつ簡潔に問題を解決することができます。先ほどのコードは以下のようなコードに書き換えることができます。
private static final String LOGIN_SQL = "SELECT * FROM user WHERE user_id = '?' AND password = '?'";
public boolean tryLogin(String userId, String password){
PreparedStatement statement = connection.prepareStatement(LOGIN_SQL);
statement.setString(1, userId);
statement.setString(2, password);
ResultSet result = statement.executeQuery();
if(!result.first()){
// ログイン失敗
return false;
}
// ログイン成功
return true;
}
値を動的にセットするこの仕組みは、あらゆる言語とドライバに広く実装されていますので、よほどの事情がない限りはこれを使うべきです。
その他の対策方法(非推奨)
様々な対策方法がありますので、一部紹介します。
- 危険文字(’など)のエスケープ処理(サニタイジング)
- WebApplicationFirewallによるパケット遮断
クロスサイトスクリプティング
サイトを閲覧したユーザーに任意のjavascriptコードを実行させられてしまう可能性がある、危険な攻撃です。
攻撃手法
掲示板などの、ユーザーが投稿した情報をそのままWebページに表示させるようなアプリケーションを考えてみましょう。多くの場合、表示部分は以下のように実装するはずです。
<p> {投稿された名前} </p>
<p> {投稿された本文} </p>
実際に投稿してみましょう。
名前 | 本文 |
---|---|
太郎 | こんにちは! |
アプリケーション側で処理すると、ソースコードは以下のようになります。
<p> 太郎 </p>
<p> こんにちは! </p>
ここに、悪意をもった攻撃者が攻撃用のスクリプトを組み込んでみます。
名前 | 本文 |
---|---|
太郎 | <script>alert("XSS");</script> |
これを処理すると、以下のようになります。
<p> 太郎 </p>
<p> <script>alert("XSS");</script> </p>
これは、正当なHTMLとして認識されてしまいますので、ここの部分が読み込まれた瞬間に、<script>
タグの内容が実行され、alertが表示されてしまいます。
今回はalertを表示するだけに留まりますが、クッキーの内容を攻撃者のサーバーにajax送信されるようなコードを書かれると、個人情報の漏洩や、セッションハイジャックなど、非常に深刻な問題を招いてしまいます。
対策
出力値のエスケープを行う
今回、問題となるのはscriptタグを囲む<>の部分ですから、これを無害化してやれば良いわけです。HTMLには実体参照というものがありますので、それを利用して、以下のように変換します。
変換元 | 変換先 |
---|---|
< | < |
> | > |
& | & |
“ | " |
処理すると、以下のようになります。
<p> 太郎 </p>
<p>`<script>alert("XSS");</script>`</p>
ブラウザで表示すると、alertが表示されないことが確認できます。
クロスサイトリクエストフォージェリ
クロスサイトスクリプティングと名前は似ていますが、全くの別物で、クエリパラメータを乗せた任意のURLを読み込ませることにより、当該Webアプリに任意の処理をさせる攻撃です。
攻撃手法
getリクエストのクエリパラメータを受けて、その内容を公開するというSNSサイトを考えてみます。
メンバーのセッションが有効な間はセッションの値を利用して、ログインしなくても書き込みができるように設計されています。
Webサイトが受け取るgetの値は次のとおりです。
?title=(タイトル)&body=(内容)
攻撃者はこれを利用して、悪意あるウェブサイトへSNSユーザーを誘導するような投稿URLを組みます。
?title=面白いサイトみつけた!&body=http://www.example.com
あとは、これを偽装して不特定多数のSNS利用者に踏ませるだけで、利用者が知らないうちに誘導記事が続々と投稿されていきます。
対策
Webアプリケーション側で、正当なフォームから送信された値であることを保証するために、トークンをhidden値に埋め込んで一緒に送信するような対策が考えられます。
実装方法は様々ですが、擬似コードだと以下のようになるでしょう。
// HTMLを生成するメソッド(テンプレートエンジンなど)
Map<String, String> tokens = new HashMap<>();
tokens.put(userId, getRandomToken());
// フォーム
<input type="hidden" name="token" value="{生成されたtokenの値}">
// リクエストの正当性判定メソッド
String token = tokens.get(userId);
tokens.remove(userId); // ワンタイムにする
if(token == null){
return false;
}
if(token != request.token){
return false;
}
return true;
これで、POSTの発行前には必ずフォームページを介する必要があり、かつワンタイムにすることによって攻撃者はtoken値を推測できなくなるので、従来の手法で攻撃することができなくなります。
その他の攻撃
他にも、アプリケーション上で対策を立てることはできませんが、有名な攻撃をいくつかご紹介します。
DoS攻撃
F5アタックなどが有名で、サービスに負荷をかけて応答不可とさせるような攻撃があります。複数の端末から攻撃を仕掛ける、DDoS攻撃なども有名です。
これは、アプリケーションサーバーに到達した時点で攻撃となってしまいますので、Webアプリのソースコード上で対策することはできません。iptablesのlimitなどを使用することになりますが、本稿ではスコープから外れるため、特に解説しません。
DNSキャッシュポイズニング
DNSサーバー間のやりとりを偽装することで、下位DNSのキャッシュを汚染し、目的のサイトに辿り着けなくする、または、悪意のあるサイトに誘導する攻撃手法です。
DNSサーバーを立てていない場合は、特に出来ることはありませんが、自社でDNSサーバーを持っている場合は注意が必要です。これもアプリケーション開発側で出来ることはないので、予備知識程度に留めておいてください。
バッファオーバーラン(バッファオーバーフロー)
ある命令の参照先が配列の境界値(length)を超えた場合に、他所で保有している変数の内容が書き換えられたりする攻撃です。
現代の主流のプログラミング言語を利用していれば、まず遭遇しません。たとえば、javaであれば境界値を超えたアドレスを参照しようとすると、IndexOutOfBoundsException
などの例外が発生します。
境界値チェックが徹底されていないような言語では気にする必要がありますが、通常、あまり心配することはないでしょう。
まとめ
Web開発者にとって、不特定多数からアクセスされるようなWebアプリケーションを作るときに重要な要素のひとつがセキュリティです。アプリケーション側に起因する攻撃も多いので、インフラエンジニアにすべて任せるというわけにもいきません。
最近では、フレームワークを利用することによって、ほとんどのセキュリティ対策をフレームワークに委譲出来るようになりましたが、攻撃の種類や手法について知っておくことは、サーバーへの攻撃に対応するためにも非常に大切なことです。
この記事は最低限の概要をまとめたにすぎませんので、興味があればぜひ調べてみてください。
コメント
ディレクトリトラバーサルのエラーはWEBのエラーコードとしてプログラむに通知はされないのでしょうか。そして同じように閲覧しているWEB画面にも。
3年前に構築したシステムでWAFを導入されてひっかけられてしまいました。
コメントありがとうございます。
ディレクトリトラバーサルは攻撃手法であり、エラーとして捕捉するコードを書いていなければ通知はされません。
Web画面上でも同様で、それを検知して表示する、というコードが書かれている必要があります。
WAFにおいても同様で、エラーとして通知される製品もあれば、そうでないものもあります。
詳しくは製品のドキュメントをご参照されることをおすすめします。