GitHub使用開始
今までプログラムはてきとーに作っててきとーに管理しててきとーに行方不明になっていたので、ようやくGitHubで管理することにした。
アカウント自体は作ってはいたんだけどね・・・
Pythonのデフォルト引数について
Pythonではデフォルト引数が使えるけど、気を付けなければならない点があったのでメモ。
結論から言えば、デフォルト引数に参照型のインスタンスを指定するのはご法度。
検証用のコードは以下の通り。
#!/usr/bin/env python # -*- coding: utf-8 -*- def func_int(param=0): param+=1 print (param) def func_str(param='default'): param += ', Value' print (param) def func_list(param=[]): param.append('data') print (param) def func_dict(param={}): value = param.setdefault('key',0) + 1 param['key'] = value print (param['key']) print ('int:') for i in range(5): func_int() print ('str:') for i in range(5): func_str() print ('list:') for i in range(5): func_list() print ('dict:') for i in range(5): func_dict()
そして結果。
int: 1 1 1 1 1 str: default, Value default, Value default, Value default, Value default, Value list: ['data'] ['data', 'data'] ['data', 'data', 'data'] ['data', 'data', 'data', 'data'] ['data', 'data', 'data', 'data', 'data'] dict: 1 2 3 4 5
int、str型は問題ない。が、list、dict型は出力が変化している。つまり、呼び出されるたびにデフォルト引数が変化していることがわかる。
そこでもう一個実験。
#!/usr/bin/env python # -*- coding: utf-8 -*- class Param: def __init__(self): print(u'Param Init') self.value = 0 def add(self): self.value += 1 def getparam(self): return self.value def func(param = Param()): param.add() print(param.getparam()) func() func() func() func() func()
結果
Param Init 1 2 3 4 5
気になるのはParamの初期化タイミング。
Paramクラスの初期化メソッドが呼ばれているのが一回だけ。つまり、最初の呼び出しの際にインスタンスが生成され、使いまわされているということになる。
関数が終了してもデリートされないわけですね。ローカル変数な感じで考えてると大変なことになりそう。
逆にintとstrがなぜ変化しないかというと、おそらく参照ではなくコピーされているからではないかと。
そこで引数をコピーしてから使ってみる。
#!/usr/bin/env python # -*- coding: utf-8 -*- import copy class Param: def __init__(self): print(u'Param Init') self.value = 0 def add(self): self.value += 1 def getparam(self): return self.value def func(param = Param()): p = copy.copy(param) p.add() print(p.getparam()) func() func() func() func() func()
結果
Param Init 1 1 1 1 1
明示的にコピーすれば内部的には問題ないことがわかる。
ただし、すでに初期済のインスタンスをコピーして使っているだけなので、引数自体は毎回初期化されているわけではないのに注意。動的に変化するデフォルト引数として関数を指定すると痛い目にあうかも。
それでもデフォルト引数使いたいんだ!って時はデフォルト引数をNoneにして、Noneだったら初期化するようにすればよさげ。
#!/usr/bin/env python # -*- coding: utf-8 -*- class Param: def __init__(self): print(u'Param Init') self.value = 0 def add(self): self.value += 1 def getparam(self): return self.value def func(param = None): if param is None: param = Param() param.add() print(param.getparam()) func() func() func() func() func()
結果
Param Init 1 Param Init 1 Param Init 1 Param Init 1 Param Init 1
まあ、引数の型が見ただけでわからなくなるけど、関数の説明に書いておけばいいので許容。
そして引数としてNoneが渡せないのに気づいた。まあ、Noneを期待することはあまりないと思うので、気にしないことにする。
結論として、数値・文字列など、コピーされるもの以外はデフォルト引数として与えると面倒なんでNone使おうぜってこと
Pythonのpickleモジュールの文字コードについて
自前でsqliteを使ったセッションハンドラは書いたんだけど、データのシリアライズで躓いたのでメモ。
データのシリアライズの方法はいろいろあると思うけど、メジャーなのはpickleなのかな。他にjsonやXMLがあるみたいだけど、とりあえずpickleで。
セッションはIDの重複チェックをする必要があったのでsqliteのuniqueキーとして管理することにした。
データは複数あるはずなので、シリアライズして保存。
データのシリアライズはうまくいったけど、デシリアライズでエラーが発生。
#!/usr/bin/env python # -*- coding: utf-8 -*-
"""pickle+sqliteのテストコード"""
import sqlite3 import pickle con = sqlite3.connect('sample.db') # selectの結果をフィールド名でアクセスできるように設定 con.row_factory = sqlite3.Row cur = con.cursor() # テーブル生成 try: cur.execute('CREATE TABLE session_tbl (session_id CHAR(40) UNIQUE, data BLOB, update_time INTEGER)') except: pass # テストなのでいったんデータを全削除 cur.execute('DELETE FROM session_tbl') session_id = 'abcd1234' save_data={'username':'user','password':'pass'} # セッション書き込み cur.execute('INSERT INTO session_tbl(session_id,data) VALUES(:id,:data)',{'id':session_id, 'data':pickle.dumps(save_data)}) con.commit() # セッション読み込み cur.execute('SELECT * FROM session_tbl WHERE session_id=:id', {'id':session_id}) session_data = cur.fetchone() load_data = pickle.loads(session_data['data']) #←ここでエラー print(save_data, load_data)
pickleでデシリアライズするときに KeyError: '\x00' エラーが発生する。
普通にpickleでシリアライズされたデータではデシリアライズがうまくいくので、保存→読み込みの時にデータがおかしくなっていると判断。
raise Exception(pickle.dumps(save_data), session_data['data'])
で比べてみたところ、こんな結果になった。
Exception: ("(dp0\nS'username'\np1\nS'user'\np2\nsS'password'\np3\nS'pass'\np4\ns.", u"(dp0\nS'username'\np1\nS'user'\np2\nsS'password'\np3\nS'pass'\np4\ns.")
なぜか読み込んだデータがunicodeに変換されている。
それ以外に相違点はなさそうなので、pickleはunicodeを読み込めないのか?
そこで、
load_data = pickle.loads(session_data['data'])
の部分を
load_data = pickle.loads(session_data['data'].encode('utf-8'))
({'username': 'user', 'password': 'pass'}, {'username': 'user', 'password': 'pass'})
Pythonはunicodeを使うのを推奨していて、3.xではデフォルトエンコードがunicodeらしい。
なのになぜpickleはunicodeが使えないんだろうか・・・
追記:
考えてみれば、pickleでunicodeが使えないのが問題ではなく、読み出したデータが自動的にunicodeに変換されていることが問題だな。
pickleは出力したデータそのままを読み込むことを前提としているだろうし。
むしろエラーは正しい
PythonでWebアプリ
去年に「みんなのPython Webアプリ編」が無料で公開されていたので、いろいろと試してみた。
Pythonは今2.xと3.xがあって、主流派2.xで3.xは後方互換がないらしい。めんどくさい。
GAEもDjangoも3.xに非対応らしいので、しばらくは2.xで行くしかないな。
そんでまあPython2.7.2のデフォルトモジュール縛りでWebアプリ作成中。
現在はsqliteを使ってセッション管理を作ったところ。
ファイルでもよかったんだけど、セッションIDが重複しないようにロックして生成する必要があるんだけどロックがデフォでないらしい。
なので、session_idをuniqueにして管理することにする。重複してたら例外投げられるから例外チェックするだけ。ラクチン
そのうちロック機構を作ってファイルにも対応したいなあ
セッションハイジャック
前回はセッションを使ってログイン状態を維持する方法について書きました。
そして、セッションには問題があるとも書きました。
今回はセッション管理するときに注意しておくべき問題点について書きます。
サーバ側のアプリはクライアントから送られてきたセッションIDを基にセッション情報を読み書きし、ログインチェックを行います。
この時、別の人が同じセッションIDを使ってサイトにアクセスしたらどうなるでしょうか。
セッション情報はセッションIDに基づいているため、同じセッションIDであれば同じ情報を参照することができるようになります。
そのため、セッションIDを知ることができれば、成り済ましができてしまいます。
この問題をセッションハイジャックといいます。
セッションIDを手に入れるにはどのような方法が考えられるでしょうか。
1.手当たり次第に入力してみる
まあこれは勘ですね。運が良ければ(悪ければ?)当たります。しかし、現実的ではないですね。
一応対策としては、セッションIDに使う文字列を増やす、長くするなど、複雑なものを使うことでしょうか。
2.生成アルゴリズムから推測する
ID生成を自前で実装したり、生成アルゴリズムがばれている場合に要注意です。
時間やユーザIDを使って生成している場合、ユーザがログインした時間をある程度知ることができれば十分に推測は可能です。
また、乱数を使っている場合でも、簡単な乱数であれば推測される恐れがあります(C言語の乱数の精度については言うまでもないでしょう)。
そもそも、セッションIDの生成は言語レベルで専用のものが実装されているので、それを使うべきです。
アルゴリズムに問題があれば修正されるでしょうし。たぶん。
3.キャプチャする
クライアント―サーバ間で通信するときにキャプチャソフトなどで盗み見る方法です。
通信をなくすわけにはいかないので、HTTPSなど暗号化した通信を行う必要があります。
4.リンクを用意しRefererを調べる
ページを遷移した時にHTTPのヘッダ(Referer)には移動元のURLが書かれています。
セッションIDはクッキーに保存できない場合はURLに付加して送信します。そのため、RefererにセッションIDがくっついているURLが送られてしまいます。
そのため、リンク先のページに送られるRefererにはセッションIDがついているURLがセットされてしまいます。
例)Referer: http://example.com?PHPSESSID=abcdefg12345
対策としてはセッションIDをURLにつけさせないことです。
そのためにはセッションをクッキーに保存するように設定します。
クッキーに保存している情報はCookie用のヘッダで送られます。
4.脆弱性を利用する
他にXSSやCSRF、SessionFixationなど、アプリの脆弱性を突いて盗まれることがあります。
これらについてはまた別の機会に。
また、万が一セッションが盗まれた時のことを考えて、パスワード変更など重要な場面ではパスワードの再入力を求めるのも有効です。
万が一セッションが盗まれていたとしても、これでパスワードが勝手に変更されたりすることはありません。
結論
セッションIDは推測しづらいものを使う
通信は暗号化して行う
セッションIDをクッキーに保存するようにする
セッションIDが漏れるような脆弱性を作らない
重要な箇所ではログインチェックを通っていてもパスワード認証を行う
セッション管理
前回はログインの認証について書いたので、今回はログインの維持についてです。
セキュリティ上、一番いいのは接続するたびにパスワード入力を求めることですが、それではユーザビリティが低すぎて無理です。やってらんねーです。
そこで、セッションというものを使います。
セッションとは、本来ログインからログアウトまでの単位のことですが、ここでいうセッションはサーバ上にデータを持たせるための方法のことです(ちょっと乱暴)
サーバはログインに成功したユーザに対してセッションIDというものを発行します。
セッションIDは、ランダムな英数字で構成された推測しづらいログイン中のユーザごとにユニークな文字列です。たいていはクッキーに PHPSESSID として持っています(PHPの場合)。
そして、サーバ側にはセッションIDに紐づいたセッション情報を持っています。
つまり、セッションIDに紐づいたセッション情報がサーバ側で持っていればログイン中、持っていなければログインしていないと判断できるわけです。
しかし、PHPではセッションは自動で生成されてしまうため、セッションを持っているかどうかということを判断できません。
セッションにはユーザIDや一時的な計算結果など、任意の情報を持たせておくことができます。
なので、セッションにユーザIDを入れておいて、接続するたびにセッションにユーザIDが入っているかどうかを確認することになります。
○ログイン
1.ログイン認証
=>失敗したらログインエラー
2.session_start()でセッション開始
3.session_regenerate_id(TRUE)でセッションIDを再生成
4.セッション変数$_SESSION['user_id']にユーザIDを入力
5.ログイン成功
○ログインチェック
1.session_start()
2.session_regenerate_id(TRUE)
3.$_SESSION['user_id']とユーザIDを比較
=>持っていない、もしくは違えばログインチェックエラー
4.ログインチェック終了
PHPではセッションを開始する際にはsession_start()を呼ぶ必要があり、呼んだ瞬間からセッションを使用できるようになります。
セッション情報は$_SESSION変数を通じて読み書きできます。なので、$_SESSION変数に連想配列としてユーザIDを持たせ、チェックのたびに比較するようにします。
session_regenerate_id()はセッションIDを変更する関数です。これはセッションアダプションやセッションハイジャックといった脆弱性対策のためですが、これはまた今度。
このように、セッションを使えばログイン状態の維持ができます。また、セッションを通じて計算結果を使いまわしたりもできます。
セッション情報はPHPの場合はデフォルトでファイルにシリアライズ(ファイルに書き込めるように変換)して書き込まれています。
PHPの場合はphp.iniのsession.save_pathに保存先が書かれています。誰かがログインするたびにそのディレクトリにファイルが増えていきます。
セッションの有効期限はデフォルトで1440秒(24分)になっていて、時間が過ぎたセッションのファイルは自動で削除されます。
セッションは便利ですが、きちんと考えてあげないと脆弱性が生まれたり、スケールアウトできなくなります。
スケールアウトとはサーバの処理能力が足りなくなったときに、サーバ増やして均等にアクセスするようにすれば全体的に処理能力あがるよね!という並列的な考えです。メニーコアみたいな。
通常、セッションはログインしたサーバ上にファイルとして情報を保存します。なので、ログインしたサーバじゃないとセッション情報を参照できないわけです。
そうなると同じサーバにばかりアクセスしなきゃならなくなって、スケールアウトした意味がなくなりますよね。
なので、セッションでログイン管理をするとサーバがたくさんあるときはネックになります。これについてはまた今度スケールアウトとかの話で書きたいと思います。
結論
ログインの維持はセッションを使えば簡単に行えることがわかりました。
しかし、セッションにはいろいろ問題があります。
次回はセッションを使ったときに発生する問題として、セッションの脆弱性について書きたいと思います。
ログイン認証
今回は最初だし、基礎の基礎でログインについて。
パスワードの認証方法やログイン状態の維持について書こうと思います。
パスワードの認証方法は単純です。
事前に登録しておいたパスワードをデータベースから引っ張り出して、比較するだけです。お手軽。
とはいえ、データベースにそのままパスワードを平文(暗号化していない状態)で保存していると万が一サーバがクラックされた場合は非常にまずいです。
そのため、パスワードは暗号して保存する必要があります。
しかし、暗号化する場合は鍵をサーバ上に持っている必要があるので、鍵がばれないように管理するのが面倒です。
そこで、ハッシュ関数を使います。
ハッシュ関数とは、「あるデータが与えられた場合にそのデータを代表する数値を得る操作、または、その様な数値を得るための関数のこと」(Wikipediaより)です。
一番簡単な例は剰余ですね。数値をある値で割った余りをハッシュ値とする方法です。
例)
7のハッシュ値:7%10=7
14のハッシュ値:14%10=4
104のハッシュ値:104%10=4
例からわかるように、ハッシュは基本的に不可逆(元に戻せない)で、同じ値を同じハッシュ関数でハッシュ化すると同じ値になります。
元に戻せないのであれば流出してもある程度安心できます。
なので、パスワードを保存するときはハッシュ化して保存、比較するときは送られてきたパスワードをハッシュ化して、ハッシュ同士で比較という形をとります。
ハッシュ関数には、md5やSHA256などがあり、PHPではhash_hmac()関数を使って呼び出すことができます。
ただし、単にハッシュ化すればいいわけではありません。
例を見ると、ハッシュ値が重複しているものがあります。それはつまり、『違うパスワードでも認証がとおってしまう』可能性があるということです。
それを利用した方法として、レインボーテーブルを作っておくという手段があります。
ハッシュ値とそのハッシュ値になるパスワードのセットを事前に作って置いて、ログインできるパスワードを見つけ出す方法です。
ハッシュから元のパスワードを見つけ出すには総当たりで探すしかないのですが、一度テーブルを作ってしまえば同じハッシュ関数に対して流用できるので、すぐにパスワードがばれてしまうことになります
特に、md5とか強度の弱いハッシュ関数だとすぐに計算できるらしいです。
そこで、レインボーテーブル対策としてsaltがあります。
saltとは、パスワードにランダムな値を付加することで同じパスワードであっても違うハッシュを生成する方法です。
そうすれば、saltごとにハッシュが変わるため、作成済みのテーブルは役に立たなくなります。
saltはアプリごとに違う値にするだけでも効果はありますが、ユーザごとに違う値をつける方が安全です。ユーザごとにハッシュ値が変わるので。
が、ユーザごとに違う値を保持しておくのは面倒です。
なので、ユーザIDもパスワードにつなげてハッシュ化します。
ユーザIDはユーザごとユニークな値(のはず)なので大丈夫です。
パスワードの暗号化にハッシュを使ったのにはもう一つ意味があります。
ハッシュは不可逆です。もとに戻すことは誰にもできません。
運営者にも無理です。つまり、運営側がこっそりパスワードを盗み出して不正ログインすることはできない、ということが保証できるわけです。
登録時にパスワードが平文で送られてきたり、パスワードを忘れたときに教えてくれたりするサイトは要注意ってことですね。
結論
・パスワードはハッシュ化して保存する
・ハッシュはユーザIDとsaltとパスワードをつなげて求める
・ハッシュ関数はSHA256など強度の強いものを使う
長くなったので、続きはまた今度