グーグル、HPKPのサポートやめるってよ

 このブログのセキュリティテストをやっていたら、「HPKPやってねーな」という診断結果が出た。Dockerfileとシェルスクリプトでそれの自動設定までやってしまうコンテナを作ろうと調べながらゴリゴリやっていた。ちょっと進めたところで見つけた記事が↓。
グーグル、「Chrome 67」でHPKPのサポートを廃止へ


 そして自動化の残骸。
envsubst '$$LINK_NAME,$$DOMAIN,$$KEY1,$$KEY2' </etc/nginx/nginx.template > /etc/nginx/nginx.conf

nginx.template

worker_processes 1;

events { worker_connections 1024; }

http {
sendfile on;
server_tokens off;

gzip on;

upstream app_servers {
server ${LINK_NAME};
}

server {
listen *:443 ssl http2;

ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;

add_header Strict-Transport-Security "max-age=15768000; includeSubdomains";
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options nosniff;
add_header Public-Key-Pins 'pin-sha256="${KEY1}"; pin-sha256="${KEY2}"; max-age=2592000; includeSubDomains';

location / {
proxy_pass http://app_servers;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
}
}
comment: 0

年始あたりにMongoDBへのランサム狙いが頻発したらしいのとその対策

http://www.barracuda.co.jp/column/detail/742
AWSに置いたMongoDBサーバにアタックが続いているという報告

https://www.mongodb.com/blog/post/how-to-avoid-a-malicious-attack-that-ransoms-your-data
公式のアタック対策をまとめたブログ記事

 で、公式のアタック対策ブログ記事が日本語化されていなかったので適当な訳を作った。ところどころ日本語が適当。

=========================================
さいきん、MongoDBのセキュリティ設定が不十分な公開インスタンスに、悪意の持った攻撃が行われていると報告が来る。攻撃者はデータベースを消し、復旧のために身代金を要求してくる。

もしあなたのデータベースがアタックされたと思っているなら、下記のステップを踏んでほしい。

これらの攻撃は、セキュリティ保護の拡張設定をMongoDBに行うことで防げる。あなたはセキュリティ設定を正しく行う必要があるが、ドキュメントがそれを手助けしてくれるだろう。以下に関係のあるドキュメント、その他役立つリソースへのリンクを記す。

・セキュリティに関しては私たちのセキュリティマニュアルがある。さらに、MongoDBユニバーシティのカリキュラムの中でもセキュリティのオンライントレーニングを行っている。
・私たちのセキュリティチェックリストを使ってほしい。認証の利用、アクセスコントロール、ネットワーク制限、その他重要なベストプラクティスについて書かれている。
・もっとも人気のあるMongoDB(RPM)インストーラは、デフォルトではアクセスをローカルホストに制限している。他のインストール手段を使う場合も、この設定を適用しろ。
+つまりネットワークに乗っける場合、MongoDBのポートを直接公開せず、リバプロ使えってこと?+
MongoDBクラウドマネージャMongoDBOpsマネージャは継続的バックアップを提供してくれる。ユーザはクラウドマネージャでインターネット公開をアラート通知するように設定できる。

・直近リリースのMongoDB3.4は認証設定を保護されていないシステムに適用できるようになっている、ダウンタイム以外で。
MongoDB Atlasは多重レベルでのセキュリティを提供している。これは堅牢なアクセスコントロールやAmazon VPCs、VPC Peering、IP whitelists、TLS/SSLによる暗号化、その他オプションの暗号化によってネットワーク隔離が施されている。
・私たちはセキュリティインシデントのあったユーザに対して脆弱性レポートを提供し、啓蒙を行っている。このあたりについてはこちら
・もしセキュリティに関するベストプラクティスに興味があれば、わたしたちのセキュリティアーキテクチャホワイトペーパを読んだり、セキュリティハブに行ってみてねん。

攻撃への対処に関しての推奨ステップ
攻撃者がデータにアクセスしてしまったかを判断するには?
・もしアクセスコントロールが正しく行われているなら、攻撃者はデータにはアクセスできていないはずです。セキュリティチェックリストを読み、アタックポイントになりそうなところをレビューしてください。
・データベースとコレクションを確認して下さい。最近のケースでは、それらがdropされ、身代金を要求する一件のデータに置き換わっていました。
・アクセスコントロールが有効になっているなら、システムログに認証されていないアクセスや不審なログが残っています。


もしあなたがセキュリティの不十分なMongoDBインスタンスを運用していて、データが漏洩していたら
・あなたが商用サポートを受けているならうちのサービスエンジニアに連絡して。
・まず最初にやるべきは、このあとの不正なアクセスを防ぐこと。さあセキュリティチェックリストだ。
usersInfoコマンドを使って、ユーザの増減が起こっていないか調べよう。
・ログを追って攻撃された時間を調べよう。データをドロップしたコマンド、ユーザをいじくったコマンド、身代金要求のデータを作ったコマンドなど。
・もし定期バックアップがあるなら最新のものでリストアできる。攻撃までのあいだに変更されたデータを調べる必要もある。もしOpsManagerCloudManagerをバックアップに使っているなら、直ちに攻撃前の状態に戻せるかもしれない。
・もしバックアップを取っていなかったり、リストアが成功しないなら、データは永久に失われたことになる。
・攻撃者がデータベースの情報を丸ごと持って行ったと仮定し、内部セキュリティの運用に気を付ける。
comment: 0

MongoDBのセキュリティを考える(続き)

MongoDBのセキュリティを考える
 前回にMongoDBのセキュリティを考えた。Pythonで、どういうことが起こるとまずいかを書いた。今回はPythonの軽量WebフレームワークFlaskでそのまずい状況が起こるかを調べた。コードを含めてまとめておく。


 起こるとまずいのは、JSONとなる辞書型のオブジェクト中のkey,valueのうちのvalueのほうに、文字列や数値での条件ではなく、式で条件が与えられてしまうこと。下記で示すような変数some_valueに文字列か数値が入れば、それは通常の一致検索に使われる。
db.members.find({"id":some_value})

some_valueに辞書型で{"$ne":""}という形で値を入れられると、SQLInjectionでいうところの" OR 't' = 't' "のごとく、多数のレコードをひっかけるような条件となってしまう。

 まずいことがFlaskで起こるかを調べるため、WebアプリケーションとしてのFlaskを使ったコードを用意した。
from flask import Flask

from flask import request

app = Flask(__name__)

form = """<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8">
</head>
<body>
<form action="/" method="POST">
ID: <input type="text" name="user" />
PW: <input type="password" name="pw" />
<input type="submit" value="send" />
</form>
</body>
</html>
"""

@app.route("/", methods=['GET', 'POST'])
def hello():
if request.method == 'GET':
return form
elif request.method == 'POST':
user = request.form.get("user")
print(request.form)
is_str = str(isinstance(user, str))
print(user)
return is_str

if __name__ == "__main__":
app.run()

上記を書いたファイルをsrv.pyとしてPythonで実行する。
python svr.py


 認証フォームを想定したもので、ルート(http://localhost:5000/)にアクセスするとID、パスワードを入れるinputが表示される。ここで入れたIDが、さきほどの辞書型の{"id":some_value}でいうところのsome_valueの位置に入る。前回でクライアントから送信された値が、サーバ側で辞書型として得られるとまずいということはわかっているので、今回はDBに問い合わせはしない。値が辞書型で得られてしまうことがあるかを確かめる。
 FlaskでPOSTされた値は、request.form.get("key_name")でえられる。key_nameに"user"と入れれば、フォームに入れた値が文字列型で得られる。これが辞書型になってしまうようなケースがあるだろうか。
 PHPやNodejsではここらでまずいことができる。だからPythonではどうだろうと試してみた。PHPやNodejsで問題が起こるのは、inputのnameが"user"から"user[$ne]"に書き換えられたとき。そのときにサーバサイドでkey_nameの値がまずい形で取り出されてしまう。ブラウザの開発者ツールで”user[$ne]"をセットしてPOSTし、サーバサイドでその値を取り出そうとしてみる。
request.form.get("user")

取り出せなーい。なぜ?print(request.form)してみればわかる。
ImmutableMultiDict([('user[$ne]', 'foo'), ('pw', 'var')])

"user"というキーのフォーム要素はなかったということ。これなら少なくとも{"id":some_value}のsome_valueの値として、条件式となる辞書型の値となることはない。

 PHPではまずいことが起きうる。それが@ITで開設されている。
「JSON文字列へのインジェクション」と「パラメータの追加」
 PHPは変数を、配列として宣言するという手順を経ずにいきなり配列として使える。かなりゆるい。それがMongoDBと組み合わせることで穴となりうる。Pythonでは起こらなかったことを考えると、MongoDBのみに非がある脆弱性とはいいがたい。

 Flaskでなら、問題はそうそう起きなさそうである。さらに一般化していえば、検索条件に使うJSONに条件として値を入れるとき、型を確認してあればInjectionのようなことはできない。SQLでいうところのプレースホルダがないのもここらが理由だろう。
comment: 0

MongoDBのセキュリティを考える

 MongoDBをけっこう積極的に使っている。そのためにセキュリティについてもある程度考えていたが、ここで改めてそれをまとめておく。SQL Injectionみたいなのがないのかってあたりなど。


 Pythonで公式のドライバであるPymongoを使って、認証突破の手掛かりになるようなことができないか試してみる。下記SQLのようなことがMongoDBでできるかどうか。
SELECT * FROM users WHERE name = '(入力値)';

SELECT * FROM users WHERE name = 't' OR 't' = 't';

 まず準備。Pymongoをインストールした上で、MongoDBを起動して、接続する。さらに、ユーザデータとなるものを一件インサートする。
from pymongo import MongoClient


client = MongoClient()
db = client.test_database

db.members.insert({"_id":"kuma", "pw":"foo"})

 これでkumaというユーザがいる状態になった。このkumaを引っ張り出す基本的なクエリは下記の通り。
member_id = "kuma"

db.members.find_one({"_id":member_id})


 下記のクエリは該当データがないのでなにも取ってこれない。
member_id = "john doe"

db.members.find_one({"_id":member_id})


 上記を書き換えて「"john doe"がIDではない」という条件を与えてみる。下記のようになるが、これでkumaも引っ張ってこれる。
member_id = {"$ne":"john doe"}

db.members.find_one({"_id":member_id})

ユーザIDを条件として入れるべきところを、「○○ではない」という条件に書き換えてしまうのがMongoDBのクラックの仕方で最たるものだろう。

 上記はmember_idに辞書型で条件を与えているのでユーザkumaも取ってこれる。これが辞書型ではなく、きちんと文字列型で与えられるなら、当然kumaは取ってこれない。
member_id = '{"$ne":"john doe"}'

db.members.find_one({"_id":member_id})

つまり、条件を与えるところは文字列型できっちり与えられるように固めて、そこを辞書型で与えられたりしないようにしておけということになる。それを考えると、やってはいけないのはクライアント側からJSONでデータを送らせて、それをなにも考えずにJSON.loadsで辞書型データにして扱ってしまうことだ。
 例として、クライアントからJSONを送られたとして、サーバ側でそれを辞書構造のデータにパースして扱うコードを書いてみる。 
ob = json.loads('{"_id":"john doe"}')

db.members.find_one({"_id":ob["_id"]})

 これがアウトなコードの例である。JSONの_idの値はクライアント側で任意のものにできる。ということは下記のようにしてしまえば、ユーザkumaはクエリにひっかかってくる。
ob = json.loads('{"_id":{"$ne":"john doe"}}')

db.members.find_one({"_id":ob["_id"]})

 ググってみるとクライアント側でフォームデータをJSONにしたいというニーズは結構あるようだが、MongoDBを使っている場合はそれは危険。
https://www.google.co.jp/search?q=form+to+json
 もし使っているWebフレームワークが、GETで送られてきた値を、空気を読んで辞書型で返してくれるなんて危なっかしい機能があったら回避策を取ろう。PythonでMongoDBを使っている場合、条件式は辞書型で与えられる。条件の値が入る部分に文字列型で値が入るということが保証されればいい。
 PHPはGETで送られてきた値を配列型にキャストして返してくることがあるので、脆弱性が生まれる要因になっている。PHPでMongoDBを使う場合は要注意。このあたりはもはやPHPの言語仕様との相性の悪さか。


 あと覚えておくべきこととしては、MongoDBにJavaScriptを渡すと実行してくれるなんて機能もある。db.members.find("this._id == "kuma")って書けばユーザkumaを取ってこれるが、悪用も考えておくべきである。まあこのJSをちゃんとJSONになるようにカーリーブラケット"{}"で囲んでやればそれでOK。あとは心配ならサーバサイドでのJS実行を止めてしまえばいい。
https://docs.mongodb.com/manual/core/server-side-javascript/
MongoDBがJSを実行できるってのは仕様であって、それが不安になるなら選択肢は用意されている。


 MongoDBを使う場合、クエリなどに入れる値の型は最低限注意するべきことだろう。文字列が入るべきところに、別の型で条件式とか入れられたりするような設計はしてはいけない。暗黙のキャストを多用せずに、型を意識してやっていけばインジェクションみたいなものは避けられるだろうと考えている。
comment: 0

C# + MongoDBでログイン認証

 ASP.NET MVC + MongoDB環境でこのブログを作っている。今回はパスワード認証あたりが平文だったので、データベース直いじりで登録済みのパスワードをSHA256ハッシュにして、認証ロジックをそれに対応させた。

参考:http://dobon.net/vb/dotnet/string/md5.html

 パスワードにさらにユーザー名を付加し、それをなにかしらの文字列を使ってハッシュ化する。下記のようなメソッドを用意した。
private static string GetSHA256 (string userName, string pw)

{
//HMAC-SHA1を計算する文字列
var s = $"{userName}@{pw}";
//キーとする文字列
var key = "somestring";

//文字列をバイト型配列に変換する
byte[] data = System.Text.Encoding.UTF8.GetBytes(s);
byte[] keyData = System.Text.Encoding.UTF8.GetBytes(key);

byte[] bs;
//HMACSHA256オブジェクトの作成
using (var hmac = new HMACSHA256(keyData))
{
//ハッシュ値を計算
bs = hmac.ComputeHash(data);
}

//byte型配列を16進数に変換
var result = BitConverter.ToString(bs).ToLower().Replace("-", "");

return result;
}


 ハッシュ値を得るメソッドは用意したので、MongoDBに保存してあるユーザー情報と照会する。
public static bool Login(string id, string pw, IResponseCookies cookies)

{
var userCollection = DbConnection.db.GetCollection<BsonDocument>("user");
var filter = Builders<BsonDocument>.Filter.Eq("_id", id);
var doc = userCollection.Find<BsonDocument>(filter).FirstOrDefault<BsonDocument>();
if (doc == null)
{
return false;
}
else
{
var sha256 = GetSHA256(doc.GetValue("_id").AsString, pw);
if (sha256 != doc.GetValue("pw").AsString)
{
return false;
}
else
{
// 認証処理が続く
comment: 0