Flaskのセキュリティ対策
Flaskではデフォルトでセキュリティ対策がなされている部分もありますが、もちろんコーディングする上で気をつけなければいけない点もあります。
このページはこちらの公式リファレンスを噛み砕いた内容になります。
Security Considerations — Flask 1.0.2 documentation
Mozillaのガイドも参考になりました。
フォームデータの送信 - ウェブ開発を学ぶ | MDN
- インジェクション対策
- クロスサイトスクリプティング(XSS/CSS)対策
- リクエスト強要 (CSRF)対策
- セキュリティヘッダ
- データベースを利用する際のインジェクション対策
- パスワードの取り扱い
- ライブラリを最新のものに保つ
- その他のセキュリティ対策
インジェクション対策
インジェクションとはユーザーの入力にコードが含まれていた時にそれをサーバー内で実行してしまうことです。
この対策にはまず受け取るデータの型を指定することが有効です。
Flaskのflask.request.args
やflask.request.form
などリクエストを格納している属性はFlaskのベースであるWerkzeugのImmutableMultiDict
です。ここから値を得るには.get()
メソッドを利用しますが、ここで型を指定できます。
get(キー, default=値が存在しなかった時の返り値, type=型指定)
type
はほとんどの場合str
かint
で十分ではないでしょうか。
もし型変換に失敗した場合は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のframe
やiframe
で呼び出されることを許可する範囲を指定します。
これを指定していないと、第三者のウェブサイトが簡単にあなたのウェブサイトを偽装できてしまいます。
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をサポートしています。
使い方は、
- 元の文字列を
bytes
型に変換する - それを
hashlib.sha256()
関数でハッシュ化する - 扱いやすいように
.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)をまとめます。