Flaskのセキュリティ対策

Flaskではデフォルトでセキュリティ対策がなされている部分もありますが、もちろんコーディングする上で気をつけなければいけない点もあります。

このページはこちらの公式リファレンスを噛み砕いた内容になります。
Security Considerations — Flask 1.0.2 documentation
Mozillaのガイドも参考になりました。
フォームデータの送信 - ウェブ開発を学ぶ | MDN

インジェクション対策

インジェクションとはユーザーの入力にコードが含まれていた時にそれをサーバー内で実行してしまうことです。

この対策にはまず受け取るデータの型を指定することが有効です。

Flaskのflask.request.argsflask.request.formなどリクエストを格納している属性はFlaskのベースであるWerkzeugのImmutableMultiDictです。ここから値を得るには.get()メソッドを利用しますが、ここで型を指定できます。

get(キー, default=値が存在しなかった時の返り値, type=型指定)

typeはほとんどの場合strintで十分ではないでしょうか。

もし型変換に失敗した場合はdefaultに指定した値が返るので、正しい値の入力をユーザーに求めましょう。

args = {'age':23, 'name':'太郎'}

args.get('age', default=-999, type=int)
#>> 23

args.get('name', default=-999, type=int)
#>> -999

.get()メソッドについてのWerkzeugのドキュメントです。
Data Structures — Werkzeug Documentation (0.14)

クロスサイトスクリプティング(XSS/CSS)対策

FlaskのテンプレートエンジンであるJinja2には自動的にエスケープする機能がついています。自分で注意しなければならない点は以下です。

  • Jinja2を利用しないでHTMLを生成する場合
  • Markupクラスを利用する場合
  • ユーザーのアップロードしたファイルを利用する場合

Jinja2のテンプレートを書く際の注意点

またJinja2でテンプレートHTMLを作成する際、タグ中に変数を入れる場合はかならずクォーテーション'"で囲います。

<!--悪い例-->
<div name={{ name }}>

<!--良い例-->
<div name="{{ name }}">
hrefとsrc属性

ただ、aタグなどのhref属性やimgタグなどのsrc属性はさらに気をつけなければなりません。

たとえば以下のような場合、

<a href="{{ url }}">

urlにこんな文字列をいれれば、任意のコードを実行できてしまいます。

<a href="javascript:〜〜〜">

これを避けるには、受け取ったurl文字列がhttp://https://で始まっていなければ無効にするのがよいでしょう。

src属性も同様です。許可するドメインや拡張子を限定しておく必要があります。(例:.jpg.pngのみ、など)

基本的にHTMLの属性値に直接ユーザーから受け取った値を埋め込むのはリスキーです。可能な限りif文などで間接的に処理をしましょう。

自分で変数を埋め込んだHTMLを用意する場合

render_template()関数の引数に変数を渡すのではなく自分で埋め込みHTMLを生成するような場合はJinja2の自動エスケープの対象とならないので注意が必要です。

安全なケース

render_template()関数の引数に変数だけを渡すような場合はJinja2が自動でエスケープするので安全です。(逆にエスケープしてから渡すとエスケープ後の形がそのまま表示されてしまいます)

name = flask.request.args.get('name')
return flask.render_template('exsample.html', name=name)
<!--example.html-->
<html>
    <body>
        {{ name }}
    </body>
</html>
安全でないケース

自分で変数を埋め込んだHTMLを生成する場合はエスケープが必要です。

危険

name = flask.request.args.get('name')
template = '<p>ようこそ' + name + 'さん!</p>'
return flask.render_template_string(template)

flask.escape()関数を使えばOKです。

安全

name = flask.request.args.get('name')
name_esc = flask.escape(name)
template = '<p>ようこそ' + name_esc + 'さん!</p>'
return flask.render_template_string(template)

こちらのページにFlaskのインジェクションのサンプルが載っています。
Injecting Flask

リクエスト強要 (CSRF)対策

リクエスト強要とは:IPA ISEC セキュア・プログラミング講座:Webアプリケーション編 第4章 セッション対策:リクエスト強要(CSRF)対策

クッキーを利用してセッション管理をする場合。Flaskではなにも対策をしていないので、上のページを参考に自分で対策を講じる必要があります。

Flaskでの設定方法はに載せています。

セキュリティヘッダ

FlaskでHTTPレスポンスヘッダを設定するにはflask.make_response()関数を使います。

引数にレスポンスボディ(ページに出力する内容)を入れれば、flask.Responseクラスのインスタンスを生成します。

response = flask.make_response(returnvalue)
response.headers[ヘッダの項目] = 内容

最後にreturn responseすればOKです。

Flaskドキュメント:Responseクラスについて
API — Flask 0.12.4 documentation
Flaskドキュメント:make_response関数について
API — Flask 0.12.4 documentation

HTTP Strict Transport Security(HSTS)

HTTPではなくHTTPSで接続するようブラウザに要求します。
Strict-Transport-Security - HTTP | MDN

以下のように設定します。

response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'

コンテンツセキュリティポリシー(CSP)

ページ上で読み込むリソースの読み込み先をホワイトリスト形式で指定します。
コンテンツセキュリティポリシー (CSP) - HTTP | MDN

上のページに例が載っていますが、たとえば自分自身のドメインとexample.comとexmaple.net(サブドメイン含む)からの読み込みを許可する場合は以下のように設定します。

response.headers['Content-Security-Policy'] = 'default-src \'self\' *.example.com *.example.net'

X-Content-Type-Options

レスポンスヘッダで指定したcontent typeに絶対に従いなさいという指示です。これがないとブラウザはsniffといって、独自で最適なcontent typeを探そうとします。それによって意図しないスクリプトが実行される恐れがあります。
X-Content-Type-Options - HTTP | MDN


以下のように設定します。

response.headers['X-Content-Type-Options'] = 'nosniff'

X-Frame-Options

HTMLのframeiframeで呼び出されることを許可する範囲を指定します。
これを指定していないと、第三者のウェブサイトが簡単にあなたのウェブサイトを偽装できてしまいます。
X-Frame-Options - HTTP | MDN

  • 一切禁止:DENY
  • 自ドメインのみ:SAMEORIGIN
  • 特定のドメインを許可:ALLOW-FROM https://example.com/

以下のように設定します。

response.headers['X-Frame-Options'] = 'SAMEORIGIN'

X-XSS-Protection

ブラウザのセキュリティ機能を利用してXSS攻撃を抑えるものです。しかしながら完全ではなく、また思わぬ誤作動の危険性もあります。とりあえず設定しておけばいいというものではありません。
ブラウザのXSSフィルタを利用した情報窃取攻撃 | MBSD Blog
X-XSS-Protection - HTTP | MDN


設定する場合は以下のように設定します。

response.headers['X-XSS-Protection'] = '1; mode=block'

Set-Cookieオプション

クッキーの設定を追加します。設定の内容はこのページをご覧ください。
HTTP Cookie - HTTP | MDN
HTTP Cookieとは (2/2):超入門HTTP Cookie - @IT

Flaskで設定するには以下の2通りの方法があります。

  • flask.Flaskクラスのインスタンス(一般的にはappとして作成済み)config属性(辞書型)にPythonの.update()メソッドなどで項目を追加する
  • flask.make_response()などで作成したResponseクラスのインスタンスに.set_cookie()メソッドで項目を追加する

#前提
app = flask.Flask(__name__)
response = flask.make_response()

#方法1
app.config.update(SESSION_COOKIE_SECURE=True,SESSION_COOKIE_HTTPONLY=True,SESSION_COOKIE_SAMESITE='Lax')

#方法2
response.set_cookie('クッキーのキー', value='クッキーの値', secure=True, httponly=True, samesite='Lax')

.set_cookie()メソッドのドキュメント
API — Flask 1.0.2 documentation
.configのドキュメント
API — Flask 1.0.2 documentation

HTTP Public Key Pinning (HPKP・公開鍵ピンニング)

SSL証明書は本来完全に信頼できることが前提です。しかし認証局も人が運営しているものなので脆弱性や問題が起こらないとは限りません。

HPKPはSSL証明書が本当に正しいのかを検証させるものです。

しかしながら設定は難しく、また失敗した場合は長期間そのサイトにアクセスできなくなる恐れがあります。しっかりとした理解の上で設定しなければなりません。ぼくには責任が負えないので、以下のサイトなどをみてください。
HTTP Public Key Pinning (HPKP) - ウェブセキュリティ | MDN
公開鍵ピンニングについて | POSTD

まとめて設定する

パスごとにいちいちレスポンスヘッダを設定する必要はありません。最初にまとめて設定しましょう。

関数を定義しておけば便利です。

def prepare_response(data):
    response = flask.make_response(data)
    response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
    response.headers['Content-Security-Policy'] = 'default-src \'self\''
    response.headers['X-Content-Type-Options'] = 'nosniff'
    response.headers['X-Frame-Options'] = 'SAMEORIGIN'
    response.headers['X-XSS-Protection'] = '1; mode=block'
    return response

こうしておけばこんなかんじでレスポンスが作成できるので便利です。

@app.route('/example')
    def example():
        response_body = flask.render_template('example.html')
        response = prepare_response(response_body)
        return response

.set_cookie()は場合によって変わると思うのでその都度設定すればいいですね。

データベースを利用する際のインジェクション対策

利用するデータベースのAPIを仕様を確認しておきましょう。もしAPIにエスケープ機能が備わっていない場合は格納前にエスケープしておくべきです。

Google Cloud Datastoreの場合

Google App EngineでデフォルトのデータベースであるCloud Datastoreを利用する場合はメソッドの引数としてユーザーからの入力を渡すことになります。この構造上SQLに比べてインジェクションが起こりにくいです。

Modelクラスを利用している場合はPropertyの追加や型の変更はできないので、さらにリスクは小さくなります。

しかしながら以下の点については念を押しておいた方がいいです。

  • 引数に渡す前に型を確定しておく

また、Jinja2に直接変数を渡す以外での入力値の利用の可能性がある場合はデータの格納前にflask.escape()しておきましょう。

SQLインジェクション

SQLを利用する場合はSQLインジェクションのリスクがあります。

SQLコマンド中にユーザーからの入力を埋め込む場合は厳格な入力チェックが必要です。必要ない文字(SQLコマンドや記号など)は全てエスケープしておきましょう。

パフォーマンスの問題もありますが、事前に選択肢を読み込んだ上でfor文やif文を組み合わせて条件分岐としてしまうことでSQLコマンド中に直接ユーザーから受け取った変数を入れないこともできます。

SQLは構造上インジェクションに弱いのでセキュリティの面では可能な限りNoSQLを利用した方がいいです。

パスワードの取り扱い

まず、そもそもSSL化は必須です。SSL化していない通信でパスワードを送信するのはカフェで大声でパスワードを読み上げているのと同じことです。そのような実装ではユーザーのブラウザにも警告が出ます。

SSL化は前提として、パスワードは万一流出しても元の内容がわからないよう、すべてハッシュ化してから保存します。

確認の際はユーザーから入力された平文のパスワードを同じ方法でハッシュ化し、保存してあるものと同じになるかどうかで判断をします。

ハッシュ化には一般的にSHA-256を利用します。これは主要な言語のライブラリには用意されているはずです。

PythonでのSHA-256

Pythonでは標準ライブラリのhashlibでSHA-256をサポートしています。

使い方は、

  1. 元の文字列をbytes型に変換する
  2. それをhashlib.sha256()関数でハッシュ化する
  3. 扱いやすいように.hexdigest()str型に戻す

となります。

import hashlib

password = "123456"

#bytes型に変換
password_bytes = password.encode()

#ハッシュ化(返り値は'_hashlib.HASH'型)
hash_bytes = hashlib.sha256(password_bytes)

#str型に変換(16進数の形になります)
hashed_password = hash_bytes.hexdigest()

なお、bytes型とstr型の変換の際は文字コードに注意する必要があります。Python3の場合はデフォルトでUTF-8になっているので問題ないですが、Python2のデフォルトはasciiです。日本語を扱えないのでUnicodeDecodeErrorが発生します。

これを防ぐにはデフォルトの文字コードを変更しておく必要があります。

# デフォルトの文字コードをutf-8に(python3ならば不要)
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

レインボーテーブル対策など

ハッシュ化では同じ文字列からは常に同じハッシュが得られるので、あらかじめ大量の文字列をハッシュ化しておいて、元の文字列とハッシュの対応表(レインボーテーブル)を作成できます。

これに対するアイデアはいくつかありますが、代表的なものを下に。

ソルト

ハッシュは1文字でも違うと全く異なるものになるため、「原文+なにか文字列」をハッシュ化する方法です。もちろん、このソルトを公開したら意味ないです。

HMAC

ハッシュ化の際に「ハッシュキー」を指定し、原文とハッシュキーのセットでハッシュを生成します。まあソルトの強力版みたいなかんじですね。
hmacライブラリもPython標準です。

import hmac
import hashlib

def createHMACSHA256(text, secretkey):
    signature = hmac.new(secretkey, text, hashlib.sha256).hexdigest()
    return signature

ライブラリを最新のものに保つ

ライブラリは定期的に更新を確認して、常に最新版を利用しましょう。
Flask · PyPI

その他のセキュリティ対策

今回はFlaskコーディングにおける一般的なセキュリティ対策をまとめました。

次回はGoogle App Engineでのセキュリティ設定(SSL、アクセス権限、ファイアウォール、Security scanner)をまとめます。