📝

Flaskのセッション管理とセキュリティについて

2021/01/23に公開

Webアプリケーションでもなんでも、個人で開発するとなると自分の好みでフレームワークやら開発環境やらを選べるところが良い。
そして私はシンプルなもの好きなので、Flaskを使う。Pythonの軽量フレームワークとしてそこそこの知名度があるはずだ。

というわけで、Flaskの紹介も兼ねつつ、自分が使用していて気になったセッション管理の話などを書いてみたい。対象読者はFlaskを使ってみたい人や触ったことがある程度の人。

Flaskの魅力

Flaskの魅力を書きたいが……私はFlaskの魅力を語れるほど、その他のWebアプリケーションフレームワークをよく知らない。
PythonでWebシステムの開発をしたいと考えた場合、現状はDjango一強のようで、軽量フレームワークを探すと自然とFlaskになったという経緯だ。
しかしこれがやってみると思った以上にシンプルで使いやすく、気に入ってしまった。シンプルなのでとっつきやすいのも良かった。

ハローワールド+αはこんな感じ。

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/', methods=['GET'])
def index():
    return 'Hello World!'

@app.route('/page', methods=['GET'])
def page():
    return render_template('page.html')
    
if __name__ == "__main__": # 直接起動用
    app.run(debug=True)

アクセス可能なURLをずらっと並べておくとサイトの設計そのものみたいになる。
debug時は直接起動でもいいが、本来はWebサーバとwsgiで連携する。

Flaskのセッション

DBと連携してユーザー管理していれば、セッションくらい自前でどうにかすることもできるわけだけれど、生兵法は脆弱性の元だし、まあ普通はフレームワークの標準の機能で実装する。
Flaskにもセッション機能が用意されているのだけれど、ログインするようなサイトだとflask_loginだけで使えたりする。

from flask import Flask, render_template, redirect, url_for
from flask_login import LoginManager, UserMixin, login_user, logout_user

app = Flask(__name__)
app.config['SECRET_KEY'] = b'~~~~'
login_manager = LoginManager()
login_manager.init_app(app)

class User(UserMixin):
    def __init__(self, uid):
        self.id = uid

@login_manager.user_loader
def load_user(uid):
    return User(uid)

@app.route('/', methods=['GET'])
def index():
    return render_template('index.html') # ログイン前のページ

@app.route('/login', methods=['POST'])
def login():
    user = User('test') # 本当はちゃんとユーザーを認証する
    login_user(user) # ログイン
    return  redirect(url_for('contents'))

@app.route('/contents', methods=['GET'])
@login_required
def contents():
    return render_template('contents.html') # ログイン後のページ

@app.route('/logout', methods=['POST'])
def logout():
    logout_user() # ログアウト
    return redirect(url_for('index'))

ただ、こうすれば使えるよ、という安易な記事だけ見ていると、実際的にどういう処理が動いているのかわからず、応用が利かなかったりする。
それに、私は自分がコーディングしたシステムを使ってみて疑問に思った。このシステム、セッション情報をどこに格納しているんだ? と。
このflask_loginのコードでも、FlaskはDBなんかと連携しなくてもセッションが扱えてしまう。

Cookieが全て

実際のところ、Flaskのセッション管理においてセッション情報は全てCookieにある。暗号化した情報をまとめてクライアントのCookieに保存してしまうのだ。

通常ならCookieに保存されるのは文字数の決まったセッションキーだと思うのだが、Flaskの場合はセッション情報そのものなので、仮にセッション情報としていろいろなデータを持たせてやると、Cookieに保存される暗号化された文字列がどんどん長くなる。

from flask import session, request
app.config['SECRET_KEY'] = b'~~~~' # これが暗号化/復号のための鍵になる

@app.route('/login', methods=['POST'])
def login():
    #こんな感じで書くとセッション情報を追加できる。
    session['username'] = request.form['username']
    session['data'] = 'hogehoge'
    return redirect(url_for('contents'))

HTTPリクエストのたびに長大なセッション情報が行き来するのは気持ち悪いので、必要最低限のユーザー情報だけにしておきたいところ。たぶんflask_loginを標準的なやり方で使った場合はユーザーIDだけセッションに追加される。

セッション情報の暗号化にはソースコード内で定義された鍵(共通鍵)が使用されるため、鍵が漏洩する可能性とか解読される可能性を考えればちょっとはあれだけれども、基本的にはセキュリティ上問題の無い仕組みだと思う。
むしろちょっとセッション管理だけしたいのにDB用意して連携するのは面倒!って時には便利じゃないだろうか。Flaskはやさしいね。

ログアウトに不備?!

さて、私は業務上Webアプリケーションのセキュリティ診断なんてことをやっていたりするのだけれど、自分がFlaskで作成したWebアプリケーションのログイン/ログアウトについて診断すると、ログアウトに不備があるという診断結果になる。
指摘される不備の内容は以下のようなもの。

  1. ログイン中のCookieを記録しておきます。
  2. ログアウトします。
  3. Cookieを復元すると、認証無しでログイン中の操作ができます。
    →ログアウト時にはサーバ側でセッションを消去してください。

……いやでも、ちゃんとログアウトの処理を書いてるんだよ?

@app.route('/logout', methods=['POST'])
def logout():
    logout_user() # ちゃんとログアウトしてる
    return redirect(url_for('index'))

ほらね、こういうときにフレームワークの動作の仕組みを理解していないと戸惑うわけだ。
私はちゃんと仕組みを勉強していて良かった、と思った。

つまり、全てのセッション情報をCookieに記録していて、サーバ側では鍵しか持っていないため、ログアウトというのはCookieのセッション情報を削除するという意味でしかない。
ログイン中のCookieを再送すれば当然ログイン中のセッションとして処理される。これは仕様通りの動きであるはずだ。
診断士の立場としては、定型的に「サーバ側でセッションを消去してください」なんて書いてしまうが、そもそもサーバ側にセッションの管理情報が無いのだから消すに消せないよね。

で、危険性は?

さて、ログアウトに関しては仕様通りの動きであることを理解した上で、じゃあセキュリティ的に問題は無いのか、というのが本題。(本題までが長い)

診断士の立場での説明としては、以下のような指摘になる。

Cookieが漏洩した場合に悪用される可能性があり、セッションの寿命が長いほど危険性が高まります。

ポイントはセッションの寿命 だ。

結論として、Flaskでセッションを使用する際はセッションの寿命を短く設定するべきだ。
しかし短く15分に設定したとして、15分おきに再ログインしてね、とユーザーに強制するのは酷だろう。ログイン中には都度セッションを更新して切れないように保つ必要が出てくる。

from flask import Flask, session
from datetime import timedelta

app = Flask(__name__)

@app.before_request
def before_request():
    # リクエストのたびにセッションの寿命を更新する
    session.permanent = True
    app.permanent_session_lifetime = timedelta(minutes=15)
    session.modified = True

そして、通常のユーザーが15分以上リクエストを送信しない可能性があるページ(こうした記事を書くページなど)では、セッションが切れないようにちょこちょこリクエストを送信してやる必要があるかもしれない。
そう考えると、セッションを短く設定しなければいけないというのは、Webアプリケーションの開発においてなかなかの足かせになりそうだ。

まあ、でも……

実際問題として、Cookieの漏洩があったとすれば、ログアウト時にサーバ側で消すような仕組みだったとしても、普通のユーザーは明示的にログアウトなんてしていないだろうから、セッションの有効期限の範囲内でアカウントは乗っ取られてしまう。
結局セッション情報が漏洩することが問題なので、漏洩したときのことなんかはあまり考えても仕方ない。とすれば、寿命を長くしておいても別にいいんじゃないかとも思う。

Discussion