主にプログラミングに関して。Python, .NET Framework(C#), JavaScript, その他いくらか。
記事にあるサンプルやコードは要検証。使用に際しては責任を負いかねます

HTTP Strict Transport SecurityがIEにやっと実装されたようなので試してみる

                
 Win8.1とWin7のIE11にHTTP Strict Transport Security(HSTS)が実装されたそうなので試してみる。
https://developer.mozilla.org/ja/docs/Security/HTTP_Strict_Transport_Security
 参考は上記リンク。HTTP Strict Transport Securityとはサーバ側からクライアントに「これ以降HTTPSでアクセスしてこいや」という命令があったときにそれに従う機能である。命令を受け入れるとそれ以降はHTTPでアクセスしようとしてもHTTPSアクセスが強制されるようになる。

 今回はいわゆるオレオレ証明書を使ってHTTPSサーバを立てる。言語はPythonでフレームワークはTornadoを使う。ページは"/"のみ存在しており、"/"へアクセスすると現在時間をヘッダにHSTSセットした状態で返す(ただしHTTPS強制が有効な時間は一分間に設定)。つまり一度"/"にアクセスすると、以降は有効期間が一分間のHSTSがセットされるというサイト。
 このサイトはHTTPSでのみ公開される。HTTPでアクセスしても返すものがないのでエラーとなる。そんなものが下記のPythonスクリプト。

import os
import datetime

import tornado.wsgi
import tornado.httpserver


HTML = """<!DOCTYPE html>
<html>
<head>
<meta>
<title>hello</title>
</head>
<body>
{0}
</body>
</html>
"""


class MainHandler(tornado.web.RequestHandler):
def get(self):
self.add_header("Strict-Transport-Security", "max-age=60; includeSubdomains")
self.write(HTML.format(datetime.datetime.now()))


if __name__ == "__main__":
application = tornado.wsgi.WSGIApplication([
("/", MainHandler),
])
ssl_settings = {
"ssl_options": {"certfile": os.path.join("crt/tor.crt"),
"keyfile": os.path.join("crt/tor.key")}
}
container = tornado.wsgi.WSGIContainer(application)
http_server = tornado.httpserver.HTTPServer(container, **ssl_settings)
http_server.listen(8888)
tornado.ioloop.IOLoop.current().start()


 上記スクリプトを走らせて、https://localhost:8888/へアクセスしてみる。ブラウザはWin7で最新アップデートを適用したIE11。
 サーバからのレスポンスとして現在時間が取得できている。
1506130030218.jpg

 レスポンスヘッダを見てみるとStrict-Transport-Securityが設定されている。
1506130031186.jpg

 Strict-Transport-Securityが設定された状態でURLのhttpsをhttpにしてアクセスしてみる。httpでアクセスしてもサーバからはエラーが返されるはずなのに、現在時間がbodyとして書かれたHTMLがサーバから返ってきている。これはHTTPSでリクエストが送られたということだろう。開発者ツールのほうではプロトコルはHTTPのままになっているが……
1506130032271.jpg

 ただのhttpアクセスではエラーが出るのを確認するため、Strict-Transport-Securityの有効期間である一分待ったのちにhttpアクセスをもう一度かけてみた。結果は下記のようにエラー。
1506130034215.jpg



 念のため仮想マシンでサーバを立て、FiddlerでキャプチャすることでHTTPアクセスをしていないか改めて確認してみた。
 SSL通信でしかコンテンツを配信してないサーバにまずHTTPSアクセスしてHTTPS強制命令を取得。後にコンテンツのあるパスを打ち込んでHTTPアクセスするとアドレスバー内のアドレスがhttpsに書き換えられた状態でページが表示された。コンテンツのないパスを打ち込んでHTTPアクセスするとアドレスはhttpのまんまでNot Foundエラー表示。 両方の場合ともFiddlerでプロトコルを確認するとHTTPSアクセスしか行っていなかった。HTTPでのアクセスは一切なし。正しい。
 さらに同じドメインへのリンクをhttp://始まりで置いてこれを踏んでみたところ、これもまたHTTPSでアクセスが行われた。HTTPS強制できている。
1506131412235.jpg
 アドレスが手打ちだろうとリンク踏みだろうと、HTTPSの強制化が確認できた。



 IE11に実装されたHSTSを試してみた。Strict-Transport-Securityをレスポンスヘッダにセットすることで以降のアクセスはHTTPSに強制されたようだ。だが一方で、Strict-Transport-Security有効状態でHTTPでアクセスしたときに、開発者ツールにはプロトコルがHTTPのままで表示された。ちょっと紛らわしい状態。

http://blogs.windows.com/msedgedev/2015/06/09/http-strict-transport-security-comes-to-internet-explorer-11-on-windows-8-1-and-windows-7/


 ちなみにSSLを使うハードルの一つにSSL証明書が有料ということがある。これが無償配布される話が出ていてとても気になる。
http://itpro.nikkeibp.co.jp/atcl/column/14/346926/121500129/
スポンサーサイト



Python: クッキーにsecure属性を付加する

                
 スラドで盗聴されたくないはずのクッキーにsecure属性が付加されていないケースがまだあるという話題があった。
http://security.slashdot.jp/story/14/12/17/0941232/HTTPSを使っているのにCookieに「secure」属性を設定していない危険なサイトが話題に
 secure属性をつけたクッキーはHTTPSでしかやり取りされなくなる。だから盗聴耐性が生まれる。一方で「うちはHTTPSでしかWebページを公開していない。だからクッキーにsecure属性をつけなくても安全だ」という理屈はとおらない。
 たとえばこのブログがHTTPSでしか公開されていないとして、"https://elicon.blog57.fc2.com/"のどっかで認証管理を済ませてsecure属性が外れたクッキーを取得したとする。このクライアントになんらかの方法で"http://elicon.blog57.fc2.com/"のリンクを踏ませる。ブラウザにはおそらくエラーが返ってくるが、リクエスト送信のときにsecure属性のついてないクッキーは必要情報として送信される。ネットワーク経路に中間者がいればそれが盗聴される。

 ここでもXSRF対策を扱った記事でクッキーにsecure属性を付加してきた。今一度Pythonのこれまで触ったWebフレームワークでどうやってクッキーにsecure属性をつけるかまとめておく。

・GoogleAppEngine(webapp2)
class Foo(webapp2.RequestHandler):
def get(self):
self.response.set_cookie("_xsrf", token, secure=True)


・Tornado
class Foo(tornado.web.RequestHandler):
def get(self):
self.set_cookie("_xsrf", token, secure=True)

 TornadoでRequestHandlerを継承したクラスを作ると、上記以外にset_secure_cookieというメソッドが使える。だがそれはクッキーにsecure属性を付加するものではないので注意。
http://tornado.readthedocs.org/en/stable/guide/security.html

・Django
 使ったことがないけどどうTornadoで書くときにどう実装してるのか参考にしたくて調べたことがある。ドキュメントの下記の”Use ‘secure’ cookies.”という項が参考になるはず。
https://docs.djangoproject.com/en/dev/topics/security/#ssl-https

Python: GoogleAppEngineのXSRF(CSRF)対策

                
 GoogleAppEngineにのっけているブログにXSRF対策を施していなかった。そんなものに対してクラックしかけて喜ぶ人なんていないだろうと思いつつ、GoogleAppEngineのwebapp2にはとくにxsrf対策が用意されていないようなので後学のためにモジュールを作っておくことにする。

# -*- coding: utf-8 -*-
import uuid
import os

if os.environ.get('SERVER_SOFTWARE','').startswith('Development'):
DEBUG = True
else:
DEBUG = False


def _check_token(handler):
"""check xsrf token equality between cookie and request parameter"""
cookie_token = handler.request.cookies.get("_xsrf")
form_token = handler.request.get("_xsrf")
if cookie_token != form_token:
return False
return True

def token_required(handler_method):
"""decorator checks xsrf token equality"""
def _token_required(handler, *args, **kwargs):
if _check_token(handler):
handler_method(handler, *args, **kwargs)
else:
handler.abort(403)
return _token_required

def set_token(handler, secure_attr=True):
"""set xsrf token cookie and return token value"""
token = str(uuid.uuid4())
if secure_attr and (not DEBUG):
handler.response.set_cookie("_xsrf", token, secure=True)
else:
handler.response.set_cookie("_xsrf", token)
return token


HOW TO USE
HTMLとクッキーにトークンを埋め込みたい場合は、関数set_tokenにリクエストハンドラselfを渡す。これでクッキーにトークンが仕込まれ、かつトークンが文字列で関数から返される。返されたトークン値をテンプレートエンジンに渡すなりなんなり。
あとはトークンの整合性を確認してから処理をしたいメソッドにデコレータtoken_requiredを付ける。これでリクエストのパラメータで渡されたトークン値とクッキーとして渡されたトークン値の整合が取れなければ403エラーを返す。整合が取れればメソッドをそのまま実行する。
class EntryHandler(webapp2.RequestHandler):
def get(self):
template_values = {"xsrf":xsrf.set_token(self)}
template = jinja_environment.get_template('add.html')
self.response.out.write(template.render(template_values))

@xsrf.token_required
def post(self):
entry = Entry()
entry.title = self.request.get('title')
entry.body = self.request.get('body').

フォームは以下のように。
<input name="_xsrf" type="hidden" value="{{ xsrf }}" />


 クッキーに埋め込まれたxsrfトークンは基本的に盗聴されていいものだと考えていないので、デフォルトではGoogleAppEngineサーバで使われるときはsecure属性を付加している。ただローカルの開発サーバでテストするときはそもそもSSL通信ができないので、このままでは開発サーバ上でのテストが実行できなくなる。開発サーバかGoogleAppEngineのパブリックなサーバかを判別してsecure属性の付加を判断している。
if secure_attr and (not DEBUG):
handler.response.set_cookie("_xsrf", token, secure=True)
else:
handler.response.set_cookie("_xsrf", token)


余談:メソッドのデコレータと引数の命名
 デコレータとして使う関数token_requiredはクラス内で使わるので、内部関数の第一引数にはインスタンスが渡されるようになっている。これをselfと書くかどうか。
def token_required(handler_method):
def _token_required(self or ???, *args, **kwargs):
...

 そもそもPythonのメソッドはその最初の引数にインスタンスが渡されることが決まっていて、それをselfという引数で受け取ってつらつら書いていくのが慣例だ。
http://docs.python.jp/2/tutorial/classes.html#tut-remarks
 あらゆるタイプのメソッドの第一引数をselfと書くわけでもないらしく、公式ドキュメントではクラスメソッドを書くときはselfでなくclsと書いている。
http://docs.python.jp/2/library/functions.html#classmethod
これを考えると、慣例的にはselfに入っているのはインスタンスへの参照のようだ。
 今回デコレータの内部関数はメソッドのように使われる。その内部関数が実行されるとき第一引数に入っているのはインスタンスへの参照だ。だから第一引数をselfと書いてコーディングするのが一般的かもしれない。しかし一方でメソッドでなく関数としてクラスの外で定義されているので、そこにselfという名前の引数があるのもモヤッとした。
 結局第一引数として渡されているインスタンスはRequestHandlerクラスのインスタンスなので、handlerという変数名にしておいた。一通り書き終えたあと、モヤッを解消するためにググってみたところ、脳内で暗黙のうちに今回書いたデコレータのモデルにしていたwebapp2のおまけモジュールのlogin_requiredではselfを使っていた。
https://webapp-improved.appspot.com/_modules/webapp2_extras/appengine/users.html
 stackoverflowではぼくと同様にモヤッたのかはわからないが、selfを使っていない例を見つけられた。
http://stackoverflow.com/questions/1123117/python-function-decorators-in-google-app-engine
どっちがいい命名プラクティスなんだろう。

MongoDB: インジェクションを考える

                
 データベースを扱う上で付いて回るのがインジェクションというセキュリティの問題である。データベースにあるデータ(些末なものからIDやパスワードまで)を見境なくぶっこぬかれたり、認証に使っている場合は認証を偽装されたりと対策をしない理由はない。MongoDBを使う場合、どういうことを気にすればいいかを@ITの記事を参考に検証する。このブログではPythonを主に扱っているので、PyMongoでMongoDBへ接続するときの諸々を検証する。


 まず前提としてアプリケーションでデータベースを使いたければ、信頼できるところが作っているドライバを使うこと。PythonとMongoDBの組み合わせならPyMongoがある。MongoDBはJavaScriptを書くことでデータの挿入、更新、削除などができる。PyMongoはこれをPythonでできるようにするものだが、基本的な部分ではJavaScriptとPythonの文法の差は現れない。JavaScriptで書いていたものをほぼそのままPythonで書くだけで使える。

 下準備としてMongoDBを立ち上げ、PyMongoを使って接続する。インジェクションで認証偽装を試みるので、sessionコレクションに、インジェクションが成功したらドキュメントが得られるように一件挿入しておく。
import pymongo

db = pymongo.MongoClient("localhost", 27017).test_db
if not db.session.find_one():
db.session.insert({"session":"session_id_str"})


とりあえず下記のコードを実行してみる。sessionコレクションから、sessionキーが文字列”x”のドキュメントを探し、件数を返す命令だ。
print db.session.find({"session":"x"}).count()

sessionキーが文字列”x”のドキュメントはないから、インタープリタには0が表示される。この条件では該当ドキュメントがsessionコレクションにはないから、認証はとおらないということ。で、正規の方法で認証をとおしたければ正規のキーを入れればいい。
print db.session.find({"session":"session_id_str"}).count()


1.演算子のインジェクション
参考:http://www.atmarkit.co.jp/ait/articles/1305/23/news004.html
PHPなどの言語には、配列/連想配列と解釈できる形式で書かれたリクエストパラメータを、プログラム内で配列/連想配列に展開する機能があります
Pythonの基本機能にはそんなことをするものはない。なのでそういう機能を暗黙のうちに提供する可能性があるのはフレームワークのほうなので、フレームワークがクライアントからPOSTされたりしたパラメータを暗黙のうちに文字列から辞書型などに変換して返すような仕様がないか確認しておく。例えばwebapp2の場合、以下のように書いておけばOK.
s_id = self.request.get("s_id")
db.session.find({"session":s_id})



2.クライアントから送られたJSONをそのまま辞書型にして使うのは避ける
 問題ない使いかた。データベースから文字列”session_id_xxx”に一致するセッションIDを持つドキュメントを探す。
s_id = "x"
print db.session.find({"session":s_id}).count()

 json.loadsを使えば、JSON形式で書かれた文字列から辞書型を作れる。使いかたによっては手っ取り早くなる。
json_str = """{"session":"x"}""" # クライアントからJSON形式で左記のデータが送られてきたとする
obj = json.loads(json_str)
print db.session.find(obj).count()

 だけど上記のように書くのはまずい。悪用されると、ドキュメント検索の条件を様々に変えることができるようになり、インジェクションのきっかけに。下記では非等価を示す演算子を使って、文字列”x”に一致しないセッションIDを持つドキュメントを検索するようになる。
injection_json = """{"session":{"$ne":"x"}}"""
obj = json.loads(injection_json)
print db.session.find(obj).count()

場合によっては変数部分はどの型が来るか管理する。クライアントから送られたJSONを辞書型に変換してそのまま使わない。
obj = json.loads(injection_json)
isinstance(obj["session"], str)
db.session.find({"session":obj["session"]})



3.サーバサイドでJavaScriptを使わないようにする
参考:http://www.atmarkit.co.jp/ait/articles/1305/23/news004_2.html
 MongoDBではJavaScriptが使える。PyMongoから使う場合でも、JavaScriptコードを埋め込むことができる。だがそれは任意のコードを埋め込むチャンスになるので使用を避ける。たとえば参考記事ではパラメータを大文字化して部分一致検索をかけているが、それはPyMongoでJavaScriptを埋め込まなくても可能。
foo = {"$regex":"foo".upper()}
print db.session.find({"foo":foo}).count()

 JavaScriptの実行を不許可にしておくのも手。
http://docs.mongodb.org/manual/reference/configuration-options/#security.javascriptEnabled



 SQLでは文の構成に文字列置換を使って問題になることがある。開発者が書いた文を手玉にとりつつ、勝手なSQL文を書かせてしまうなど。
SELECT * FROM users WHERE name = '(入力値)';

SELECT * FROM users WHERE name = 't' OR 't' = 't';
上記はWikipediaから部分的に抜粋した。
 PyMongoを使って適切に書けば上記のようなことは起こりづらい。
db.users.find({"name":input_value})
ただやっぱり油断していると付け入るところはあるようで、evalやexecはそもそも使わないものとして、json.loadsもちょっと使用を差し控えたほうが無難だと考えられる。使いたければ型チェックなどの厳重に設計されたバリデーションを経ること。あとデフォルトではJavaScriptが動くようになっているので、それは切ってしまうのが手っ取り早い。

HTML5, JavaScript: Base64エンコードされた画像をアップロードする上で

                
 HTMLでimgタグを使って画像を表示したい場合、srcに画像のURLを入れる。これが近年ではcanvasタグやFile APIの導入にともなって、Base64エンコードされた文字列を画像データとしてsrcに入れるケースもブログなどで紹介されている。もしこれをユーザに任意の画像をアップロードさせるWebアプリに使うならXSSに気を払わなければならないという話。たとえばcanvasに画像を描いてもらって、それをtoDataURLメソッドで文字列化してアップロードして使うとか。

 実例として簡単なPythonサーバスクリプトを用意してみる。/postにアクセスするとフォームが用意されており、ここから画像ファイルをアップロードできる。アップロードしたファイルは成功していれば/profileで確認できる。SNSで自分のアイコン画像をアップロードすることをすごく簡単に模擬してみた。画像はDataURL形式で文字列化して使われている。
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import wsgiref.simple_server

import tornado.web
import tornado.wsgi
import pymongo


class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hell!")

class IconPostHandler(tornado.web.RequestHandler):
def get(self):
html = """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
</head>
<body>
<input type="file" id="files" />
<button onclick="post();">post</button>
<script>
function post() {
var f = document.getElementById("files").files[0]; // FileList object

// Only process image files.
if (!f.type.match('image.*')) {
return;
}

var reader = new FileReader();

// Closure to capture the file information.
reader.onload = (function(theFile) {
return function(e) {
$.post("/post", {imageData:e.target.result});
};
})(f);

// Read in the image file as a data URL.
reader.readAsDataURL(f);
}
</script>
</body>
</html>"""
self.write(html)

def post(self):
image_data = self.get_argument("imageData")
db.user.save({"_id":"user1", "icon":image_data})

class ProfileHandler(tornado.web.RequestHandler):
def get(self):
user = db.user.find_one()
html = """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
user ID: {userID}<br>
icon: <img width="50" src="{icon}" />
</body>
</html>""".format(userID=user["_id"], icon=user["icon"])
self.write(html)

db = pymongo.MongoClient("localhost", 27017).test_db

if __name__ == "__main__":
settings = dict(
debug=True,
)

application = tornado.wsgi.WSGIApplication([
(r"/", MainHandler),
(r"/post", IconPostHandler),
(r"/profile", ProfileHandler),
], **settings)

server = wsgiref.simple_server.make_server('', 8888, application)
server.serve_forever()


 /postにアクセスして画像をアップロードして/profileでそれを表示する。これは悪意のない優しい使いかた。ここでブラウザの開発者メニューからコンソールを開いて、下記の命令を入れてみる。
$.post("/post", {imageData:'" /><script>alert("foo");</script>'});
そんで/profileにアクセスしてみる。画像は表示されずに、アラートでfooという文字列が表示される。なにも考えずにDataURL形式で画像を取りまわしてHTMLに埋め込んだりしようとすると、任意のスクリプトが埋め込めてしまうということ。テンプレートエンジンのオートエスケープなどで防がれることもあるだろうが、うっかり画像ファイルデータならエスケープしなくてもいいだろうとオートエスケープを外してしまうかもしれない。
 対策としてエスケープしておけばいいが、そもそもDataURL形式で画像を扱うのはそれほどメリットになることでもない。データ量がバイナリに比べておおよそ30%増しだし、埋め込んでしまうことでキャッシュも効かなくなる。画像はDataURL形式で取りまわすことなく、バイト列で旧来どおりにやることがおそらく無難。どうしてもDataURL形式で取りまわしたいならXSS対策は打っておく必要がある。
プロフィール

h

Author:h

最新記事
リンク
作ったものなど
月別アーカイブ
カテゴリ
タグリスト

検索フォーム
Amazon