Angular + HttpClientXsrfModule + Flask で CSRF

Angular と Flaskを使って、CSRFを実現しようとしたのだが、若干手数がかかったのでメモ。

1.Angular + HttpClientXsrfModule

Angularの全体像から、コンセプト、豊富な実例と必携の良書

Angular で、CSRFを実装しようとしてググってみたりしたのだが、サンプルとして言及されている、XSRFStrategy は、Deprecated で、@angular/common/http を代わりに使用するようにとのこと。

https://angular.io/api/http/XSRFStrategy

そもそも @angular/http が、Deprecated なんですね。こちらも、common/httpを使うように書かれていました。

https://angular.io/api/http/HttpModule  こちらではなく、

1.1 モジュール宣言

https://angular.io/guide/http#security-xsrf-protection に沿って実装する。

まず、以下の2つのモジュールを app.module に import。

https://angular.io/api/common/http/HttpClientModule

https://angular.io/api/common/http/HttpClientXsrfModule

サーバー側でCookie にセットした、Cookie名「XSFR-TOKEN」のCSRFトークンを、リクエストヘッダーにキー名「X-XSRF-TOKEN」として返すことを、HttpClientModule、HttpClientXsrfModule の宣言をするだけで自動でやってくれる。

上記のキー名を変更したい場合、imports セクションで、withOption に別名を指定する。下のコードではコメントアウトしている。

app.module

  1. import { HttpClientModule, HttpClientXsrfModule } from '@angular/common/http';
  2. @NgModule({
  3. declarations: [
  4. :
  5. ],
  6. imports: [
  7. :
  8. HttpClientModule,
  9. HttpClientXsrfModule, /*.withOptions({ cookieName: 'XSRF-TOKEN', headerName: 'X-XSRF-TOKEN'})*/
  10. ],
  11. providers: [],
  12. bootstrap: [AppComponent]
  13. })
  14. export class AppModule { }
  15.  

1.2 サービスの実装

HttpClient を importし、リクエストを送信。とりあえず、今回は、CSRFを適用するリクエストは POSTのみ対応とするため、POSTで送信。

  1. import { Injectable } from '@angular/core';
  2. import { HttpClient } from '@angular/common/http'
  3.  
  4. @Injectable()
  5. export class HogeService {
  6.  
  7. configureRequest(req: Request): void {
  8. }
  9.  
  10. constructor(private http: HttpClient) { }
  11.  
  12. hello(name: string){
  13. this.http.post('/api/hello',
  14. JSON.stringify({ params: { name: name } })
  15. ).subscribe(
  16. response => {
  17. console.log(response);
  18. },
  19. error => {
  20. console.log(error);
  21. }
  22. );
  23. }
  24. }

Angular側は以上。

2.Flask

Flaskのコードスニペットを参考にCSRFを実装する。

http://flask.pocoo.org/snippets/3/

この例では、HTMLの hidden にキーを設定するのだが、Angular で生成したHTMLファイルを、Flaskのテンプレートとして、Flaskの変数を埋め込むのは難儀、かつ、上記Angularの戦略として、CookieにCSRFトークンを埋め込んで渡す。Cookieの作成のスニペットは以下。

http://flask.pocoo.org/snippets/30/

2.1 注意点など

app.secret_key = ‘hogehoge’

session を使用するときに、secret_key に任意の値を設定しないとエラーとなる。

sessionには、最初に画面(index.html) を返す時に生成したCSRFトークンを格納する

redirect_ui_index_with_crsftoken()

Angularが生成した、index.html にリダイレクトし、Http Responseに、”XSRF-TOKEN”を設定して返す

name = request.json[‘params’][‘name’]

application/json として送信されたデータは、Flask で、request.json として解析済み結果を取得できる。

def server_error(e)

共通のエラーハンドラー。@app.errorhandlerで対応するエラーコードを指定。

csrf_protect()

@app.before_request デコレーターで、リクエストの前処理。POSTの場合、sessionから、”XSRF-TOKEN”を取得。リクエストヘッダーとして送信された、”X-XSRF-TOKEN”の値と比較し、一致すればリクエストを受け付ける。

不一致の場合、不正アクセスとして403エラーとする。

generate_csrf_token()

sessionにCSRF_TOKENが含まれていない場合、生成(UUID)し格納。

  1. import logging
  2. import uuid
  3.  
  4. from flask import Flask, render_template, request, redirect, session, abort, jsonify
  5.  
  6. CSRF_TOKEN = '_csrf_token'
  7.  
  8. app = Flask(__name__, static_folder='app')
  9. app.secret_key = 'hogehoge'
  10.  
  11. @app.route('/', methods=['GET'])
  12. def redirect_ui_index_with_crsftoken():
  13. response = app.make_response(redirect("/hoge/index.html"))
  14. response.set_cookie('XSRF-TOKEN', value=generate_csrf_token())
  15. return response
  16.  
  17. @app.route('/api/hello', methods=['POST'])
  18. def api_hello():
  19. name = request.json['params']['name']
  20. return jsonify({"result":{"name":name}})
  21.  
  22. @app.errorhandler(403)
  23. @app.errorhandler(500)
  24. def server_error(e):
  25. logging.exception(e)
  26. return 'an error occurred.', e.code
  27.  
  28. @app.before_request
  29. def csrf_protect():
  30. if request.method == "POST":
  31. token = session[CSRF_TOKEN]
  32. if not token or token != request.headers.get('X-XSRF-TOKEN'):
  33. abort(403)
  34.  
  35. def generate_csrf_token():
  36. if CSRF_TOKEN not in session:
  37. session[CSRF_TOKEN] = str(uuid.uuid4())
  38. return session[CSRF_TOKEN]

以上

Follow me!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です